테마 적용
CI / test (push) Failing after 5m22s
CI / e2e (push) Has been skipped
CI / pr_and_merge (push) Has been skipped

This commit is contained in:
2025-12-09 18:53:21 +09:00
parent 9d8c4538c7
commit 676e58cad7
71 changed files with 3394 additions and 498 deletions
+13 -4
View File
@@ -81,8 +81,10 @@ export const buildStaticHtml = (blocks: Block[], projectConfig?: ProjectConfig):
const basePart = "margin:0 auto;padding:24px;box-sizing:border-box;";
const canvasStyle = `${widthPart}${bgPart}${basePart}`;
const bodyBgRaw = (projectConfig?.bodyBgColorHex ?? "#020617").trim() || "#020617";
const bodyStyle = `background-color:${bodyBgRaw};margin:0;padding:0;`;
const bodyBgRaw =
typeof projectConfig?.bodyBgColorHex === "string" ? projectConfig.bodyBgColorHex.trim() : "";
const bodyBgPart = bodyBgRaw ? `background-color:${bodyBgRaw};` : "";
const bodyStyle = `${bodyBgPart}margin:0;padding:0;`;
const trackingRaw = (projectConfig?.trackingScript ?? "").trim();
const trackingHtml = trackingRaw ? `\n${trackingRaw}\n` : "";
@@ -460,12 +462,19 @@ export const buildStaticHtml = (blocks: Block[], projectConfig?: ProjectConfig):
const inputId = name;
const fieldClass = isFloating ? "pb-form-field pb-form-field--floating" : "pb-form-field";
let fieldStyleAttr = "";
const cssVars: string[] = [];
if (isFloating && typeof props.paddingY === "number" && props.paddingY >= 0) {
const em = props.paddingY / 16;
fieldStyleAttr = ` style="--pb-input-padding-y:${em}rem"`;
cssVars.push(`--pb-input-padding-y:${em}rem`);
}
// strokeColorCustom 이 있으면 플로팅 라벨 패치 색으로 사용할 CSS 변수에 전달한다.
if (typeof props.strokeColorCustom === "string" && props.strokeColorCustom.trim() !== "") {
cssVars.push(`--pb-input-border-color:${escapeAttr(props.strokeColorCustom.trim())}`);
}
const fieldStyleAttr = cssVars.length > 0 ? ` style="${cssVars.join(";")}"` : "";
const placeholderBase =
typeof props.placeholder === "string" && props.placeholder.trim() !== ""
? props.placeholder.trim()
+7 -3
View File
@@ -70,7 +70,7 @@ export function EditorCanvas(props: EditorCanvasProps) {
return (
<div
className="flex-1 p-4 flex flex-col gap-2 text-sm text-slate-200 border-r border-slate-800 overflow-auto pb-scroll"
className="flex-1 p-4 flex flex-col gap-2 text-sm border-r border-slate-800 overflow-auto pb-scroll"
data-testid="editor-canvas"
style={canvasOuterStyle}
onClick={(event) => {
@@ -118,11 +118,11 @@ interface DragPreviewProps {
block: Block | null;
}
function DragPreview({ block }: DragPreviewProps) {
export function EditorCanvasDragPreview({ block }: DragPreviewProps) {
if (!block) return null;
return (
<div className="pointer-events-none rounded border border-sky-500 bg-slate-900/80 px-3 py-2 text-xs text-slate-100 shadow-lg opacity-90 min-w-[160px] max-w-[260px]">
<div className="pointer-events-none rounded border border-sky-500 bg-white px-3 py-2 text-xs text-slate-900 shadow-lg opacity-90 min-w-[160px] max-w-[260px] dark:bg-slate-900 dark:text-slate-100">
<div className="text-[10px] uppercase tracking-wide text-sky-300 mb-1">
{block.type === "text"
? "Text"
@@ -152,3 +152,7 @@ function DragPreview({ block }: DragPreviewProps) {
</div>
);
}
function DragPreview({ block }: DragPreviewProps) {
return <EditorCanvasDragPreview block={block} />;
}
@@ -18,12 +18,12 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
return (
<div className="space-y-3 text-xs">
<h3 className="text-[11px] font-semibold text-slate-200"> </h3>
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h3>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full 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"
value={checkboxProps.groupLabelMode ?? "text"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -37,9 +37,9 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full 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"
value={groupLabelDisplay}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -53,10 +53,10 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
</label>
{groupLabelDisplay === "visible" && (
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
<label className="flex flex-col gap-1 text-[11px] text-slate-500 dark: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"
className="w-full 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"
value={checkboxProps.labelLayout ?? "stacked"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -70,10 +70,10 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
</label>
)}
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
<label className="flex flex-col gap-1 text-[11px] text-slate-500 dark: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"
className="w-full 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"
value={checkboxProps.optionLayout ?? "stacked"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -109,9 +109,9 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
)}
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={checkboxProps.groupLabel ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -133,7 +133,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
<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"
className="w-full 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"
value={source}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -150,7 +150,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
<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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={checkboxProps.groupLabelImageUrl ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -208,9 +208,9 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
})()}
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={checkboxProps.formFieldName ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -222,10 +222,10 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-slate-400"></span>
<span className="text-slate-500 dark:text-slate-400"></span>
<button
type="button"
className="rounded border border-slate-700 bg-slate-900 px-2 py-0.5 text-[10px] text-slate-200 hover:bg-slate-800"
className="rounded border border-slate-300 bg-white px-2 py-0.5 text-[10px] text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={() => {
const next = Array.isArray((checkboxProps as any).options)
? [...(checkboxProps as any).options]
@@ -244,9 +244,9 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
</div>
<div className="space-y-2">
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full 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"
value={(() => {
if (checkboxProps.optionImageSource) return checkboxProps.optionImageSource;
const first = (((checkboxProps as any).options ?? []) as any[])[0];
@@ -266,9 +266,9 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
{(((checkboxProps as any).options ?? []) as any[]).map((opt, index) => (
<div key={opt.value ?? index} className="space-y-1 rounded border border-slate-800 p-2">
<div className="flex items-center gap-1">
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] items-center gap-1">
<input
className="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="flex-1 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"
placeholder="라벨"
value={opt.label ?? ""}
onChange={(e) => {
@@ -283,7 +283,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
}}
/>
<input
className="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="flex-1 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"
placeholder="값(value)"
value={opt.value ?? ""}
onChange={(e) => {
@@ -299,7 +299,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
/>
<button
type="button"
className="h-7 w-7 rounded border border-slate-700 bg-slate-950 text-[10px] text-slate-300 hover:bg-red-900/60 hover:border-red-700"
className="h-7 w-7 rounded border border-slate-300 bg-white text-[10px] text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-red-900/60 dark:hover:border-red-700"
onClick={() => {
const next = (((checkboxProps as any).options ?? []) as any[]).filter((_, i) => i !== index);
updateBlock(selectedBlockId, {
@@ -320,7 +320,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
<>
{source === "url" && (
<input
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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"
placeholder="옵션 이미지 URL (선택)"
value={opt.labelImageUrl ?? ""}
onChange={(e) => {
@@ -337,7 +337,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
)}
{source === "upload" && (
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark:text-slate-400"> </span>
<input
type="file"
accept="image/*"
@@ -397,7 +397,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
</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>
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h4>
{/* px 기반 체크박스 타이포 입력값을 노출하여 em 스케일 변환을 위한 근거를 남긴다 */}
<NumericPropertyControl
label="체크박스 텍스트 크기 (px)"
@@ -475,7 +475,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
<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"
className="w-full 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"
value={checkboxProps.widthMode ?? "full"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -574,7 +574,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
value={
checkboxProps.textColorCustom && checkboxProps.textColorCustom.trim() !== ""
? checkboxProps.textColorCustom
: "#f9fafb"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
@@ -595,7 +595,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
value={
checkboxProps.fillColorCustom && checkboxProps.fillColorCustom.trim() !== ""
? checkboxProps.fillColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
@@ -616,7 +616,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
value={
checkboxProps.strokeColorCustom && checkboxProps.strokeColorCustom.trim() !== ""
? checkboxProps.strokeColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
+34 -34
View File
@@ -76,10 +76,10 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
</p>
<div className="space-y-2">
<div className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<label className="flex flex-col gap-1">
<span className="text-slate-500 dark: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"
className="w-full 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"
value={formProps.submitTarget ?? "internal"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -90,14 +90,14 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
<option value="internal"> (Webhook )</option>
<option value="webhook"> Webhook / Google Sheets </option>
</select>
</div>
</label>
{formProps.submitTarget === "webhook" && (
<div className="space-y-3">
<label className="flex flex-col gap-1">
<span className="text-slate-400">Webhook URL (: Google Apps Script URL)</span>
<span className="text-slate-500 dark:text-slate-400">Webhook URL (: Google Apps Script 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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
placeholder="예: https://script.google.com/macros/s/.../exec"
value={formProps.destinationUrl ?? ""}
onChange={(e) =>
@@ -108,9 +108,9 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
/>
</label>
<label className="flex items-center justify-between gap-2">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark:text-slate-400"> </span>
<select
className="w-40 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-40 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"
value={formProps.payloadFormat ?? "form"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -123,9 +123,9 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
</select>
</label>
<label className="flex items-center justify-between gap-2">
<span className="text-slate-400">HTTP </span>
<span className="text-slate-500 dark:text-slate-400">HTTP </span>
<select
className="w-28 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-28 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"
value={formProps.method ?? "POST"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -138,9 +138,9 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400">Authorization ()</span>
<span className="text-slate-500 dark:text-slate-400">Authorization ()</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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
placeholder="예: Bearer xxxxxx"
value={formProps.headers?.Authorization ?? ""}
onChange={(e) =>
@@ -154,9 +154,9 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> (key=value , )</span>
<span className="text-slate-500 dark:text-slate-400"> (key=value , )</span>
<textarea
className="w-full h-16 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] font-mono outline-none focus:border-sky-500"
className="w-full h-16 rounded border border-slate-300 bg-white px-2 py-1 text-[11px] font-mono text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
placeholder={"예:\nsource=landing\nformId=contact-hero"}
value={Object.entries(formProps.extraParams ?? {})
.map(([k, v]) => `${k}=${v}`)
@@ -183,7 +183,7 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
<div className="flex justify-end">
<button
type="button"
className="inline-flex items-center gap-1 rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] text-slate-100 hover:bg-slate-800"
className="inline-flex items-center gap-1 rounded border border-slate-300 bg-white px-2 py-1 text-[11px] text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={() => setShowSheetsGuide(true)}
>
Google Sheets
@@ -194,11 +194,11 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
</div>
<div className="space-y-2 border-t border-slate-800 pt-3 mt-4">
<h3 className="text-[11px] font-semibold text-slate-200"> </h3>
<label className="flex flex-col gap-1 text-xs text-slate-400">
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h3>
<label className="flex flex-col gap-1 text-xs text-slate-500 dark: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"
className="w-full 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"
value={formProps.formWidthMode ?? "auto"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -253,11 +253,11 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
</div>
<div className="space-y-2 border-t border-slate-800 pt-3 mt-4">
<h3 className="text-[11px] font-semibold text-slate-200"> </h3>
<label className="flex flex-col gap-1 text-xs text-slate-400">
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h3>
<label className="flex flex-col gap-1 text-xs text-slate-500 dark:text-slate-400">
<span> </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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
placeholder="예: 성공적으로 전송되었습니다."
value={formProps.successMessage ?? ""}
onChange={(e) =>
@@ -267,10 +267,10 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
}
/>
</label>
<label className="flex flex-col gap-1 text-xs text-slate-400">
<label className="flex flex-col gap-1 text-xs text-slate-500 dark:text-slate-400">
<span> </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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
placeholder="예: 전송 중 오류가 발생했습니다."
value={formProps.errorMessage ?? ""}
onChange={(e) =>
@@ -283,7 +283,7 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
</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>
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h3>
<fieldset className="space-y-2" aria-label="폼 필드 매핑">
<legend className="text-[11px] text-slate-400 mb-1"> </legend>
@@ -313,11 +313,11 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
return (
<label
key={fieldId}
className="flex items-center gap-2 text-[11px] text-slate-200"
className="flex items-center gap-2 text-[11px] text-slate-800 dark:text-slate-200"
>
<input
type="checkbox"
className="h-3 w-3 rounded border-slate-600 bg-slate-950"
className="h-3 w-3 rounded border border-slate-300 bg-white text-slate-900 dark:border-slate-600 dark:bg-slate-900"
checked={checked}
onChange={(e) => {
const current = formProps.fieldIds ?? [];
@@ -334,7 +334,7 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
<label className="inline-flex items-center gap-1 text-[11px] text-slate-400">
<input
type="checkbox"
className="h-3 w-3 rounded border-slate-600 bg-slate-950"
className="h-3 w-3 rounded border border-slate-300 bg-white text-slate-900 dark:border-slate-600 dark:bg-slate-900"
checked={required}
onChange={(e) => {
const current = formProps.requiredFieldIds ?? [];
@@ -357,7 +357,7 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
<div className="space-y-1">
<span className="text-[11px] text-slate-400">Submit </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"
className="w-full 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="Submit 버튼"
value={formProps.submitButtonId ?? ""}
onChange={(e) =>
@@ -390,10 +390,10 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
</div>
{showSheetsGuide && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70">
<div className="w-full max-w-xl rounded border border-slate-800 bg-slate-950 px-4 py-3 shadow-lg">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
<div className="w-full max-w-xl rounded border border-slate-200 bg-white px-4 py-3 text-xs text-slate-900 shadow-lg dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100">
<div className="mb-2 flex items-center justify-between">
<h4 className="text-[11px] font-semibold text-slate-100">Google Sheets </h4>
<h4 className="text-[11px] font-semibold">Google Sheets </h4>
<button
type="button"
className="text-[11px] text-slate-400 hover:text-slate-100"
@@ -403,7 +403,7 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
</button>
</div>
<p className="text-[11px] text-slate-400 leading-relaxed">
<p className="text-[11px] text-slate-500 dark:text-slate-400 leading-relaxed">
, Google Apps Script URL .
.
</p>
@@ -413,9 +413,9 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
<li>"배포 → 새 배포" , URL(/exec) Webhook URL .</li>
</ol>
<div className="mt-2 space-y-1">
<span className="text-[11px] text-slate-400"> Apps Script </span>
<span className="text-[11px] text-slate-500 dark:text-slate-400"> Apps Script </span>
<textarea
className="w-full h-40 rounded border border-slate-800 bg-slate-950 px-2 py-1 text-[11px] font-mono text-slate-200"
className="w-full h-40 rounded border border-slate-300 bg-white px-2 py-1 text-[11px] font-mono text-slate-900 outline-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
readOnly
value={sheetsScriptExample}
/>
@@ -18,11 +18,11 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
return (
<div className="space-y-3 text-xs">
<h3 className="text-[11px] font-semibold text-slate-200"> </h3>
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h3>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full 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"
value={inputProps.labelMode ?? "text"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -35,9 +35,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={inputProps.label ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -47,9 +47,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full 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"
value={labelDisplay}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -63,10 +63,10 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
</select>
</label>
{labelDisplay === "visible" && (
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
<label className="flex flex-col gap-1 text-[11px] text-slate-500 dark: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"
className="w-full 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"
value={inputProps.labelLayout ?? "stacked"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -103,9 +103,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
{inputProps.labelMode === "image" && (
<>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> URL</span>
<span className="text-slate-500 dark: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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={inputProps.labelImageUrl ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -115,9 +115,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={inputProps.labelImageAlt ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -129,9 +129,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
</>
)}
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={inputProps.formFieldName ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -141,9 +141,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full 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="필드 타입"
value={inputProps.inputType ?? "text"}
onChange={(e) =>
@@ -158,9 +158,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400">Placeholder</span>
<span className="text-slate-500 dark: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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="Placeholder"
value={inputProps.placeholder ?? ""}
onChange={(e) =>
@@ -171,10 +171,10 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
/>
</label>
<label className="inline-flex items-center gap-2">
<span className="text-slate-500"> .</span>
<span className="text-slate-500 dark: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>
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h4>
<NumericPropertyControl
label="필드 텍스트 크기 (px)"
unitLabel="(px)"
@@ -248,10 +248,10 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
} as any);
}}
/>
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
<label className="flex flex-col gap-1 text-[11px] text-slate-500 dark: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"
className="w-full 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"
value={inputProps.align ?? "left"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -264,10 +264,10 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
<option value="right"></option>
</select>
</label>
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
<label className="flex flex-col gap-1 text-[11px] text-slate-500 dark: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"
className="w-full 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="너비"
value={inputProps.widthMode ?? "full"}
onChange={(e) =>
@@ -346,7 +346,7 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
value={
inputProps.textColorCustom && inputProps.textColorCustom.trim() !== ""
? inputProps.textColorCustom
: "#f9fafb"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
@@ -367,7 +367,7 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
value={
inputProps.fillColorCustom && inputProps.fillColorCustom.trim() !== ""
? inputProps.fillColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
@@ -388,7 +388,7 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
value={
inputProps.strokeColorCustom && inputProps.strokeColorCustom.trim() !== ""
? inputProps.strokeColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
@@ -18,12 +18,12 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
return (
<div className="space-y-3 text-xs">
<h3 className="text-[11px] font-semibold text-slate-200"> </h3>
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h3>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full 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"
value={radioProps.groupLabelMode ?? "text"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -37,9 +37,9 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full 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"
value={groupLabelDisplay}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -53,10 +53,10 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
</label>
{groupLabelDisplay === "visible" && (
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
<label className="flex flex-col gap-1 text-[11px] text-slate-500 dark: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"
className="w-full 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"
value={radioProps.labelLayout ?? "stacked"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -70,10 +70,10 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
</label>
)}
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
<label className="flex flex-col gap-1 text-[11px] text-slate-500 dark: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"
className="w-full 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"
value={radioProps.optionLayout ?? "stacked"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -109,9 +109,9 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
)}
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={radioProps.groupLabel ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -133,7 +133,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
<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"
className="w-full 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"
value={source}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -150,7 +150,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
<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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={radioProps.groupLabelImageUrl ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -208,9 +208,9 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
})()}
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={radioProps.formFieldName ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -222,10 +222,10 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-slate-400"></span>
<span className="text-slate-500 dark:text-slate-400"></span>
<button
type="button"
className="rounded border border-slate-700 bg-slate-900 px-2 py-0.5 text-[10px] text-slate-200 hover:bg-slate-800"
className="rounded border border-slate-300 bg-white px-2 py-0.5 text-[10px] text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={() => {
const next = Array.isArray((radioProps as any).options)
? [...(radioProps as any).options]
@@ -244,9 +244,9 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
</div>
<div className="space-y-2">
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full 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"
value={(() => {
if (radioProps.optionImageSource) return radioProps.optionImageSource;
const first = (((radioProps as any).options ?? []) as any[])[0];
@@ -266,9 +266,9 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
{(((radioProps as any).options ?? []) as any[]).map((opt, index) => (
<div key={opt.value ?? index} className="space-y-1 rounded border border-slate-800 p-2">
<div className="flex items-center gap-1">
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] items-center gap-1">
<input
className="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="flex-1 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"
placeholder="라벨"
value={opt.label ?? ""}
onChange={(e) => {
@@ -283,7 +283,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
}}
/>
<input
className="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="flex-1 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"
placeholder="값(value)"
value={opt.value ?? ""}
onChange={(e) => {
@@ -299,7 +299,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
/>
<button
type="button"
className="h-7 w-7 rounded border border-slate-700 bg-slate-950 text-[10px] text-slate-300 hover:bg-red-900/60 hover:border-red-700"
className="h-7 w-7 rounded border border-slate-300 bg-white text-[10px] text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-red-900/60 dark:hover:border-red-700"
onClick={() => {
const next = (((radioProps as any).options ?? []) as any[]).filter((_, i) => i !== index);
updateBlock(selectedBlockId, {
@@ -320,7 +320,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
<>
{source === "url" && (
<input
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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"
placeholder="옵션 이미지 URL (선택)"
value={opt.labelImageUrl ?? ""}
onChange={(e) => {
@@ -337,7 +337,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
)}
{source === "upload" && (
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark:text-slate-400"> </span>
<input
type="file"
accept="image/*"
@@ -397,7 +397,7 @@ export function FormRadioPropertiesPanel({ 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>
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h4>
{/* 라디오 타이포 px 값을 직접 입력받아 em 스케일 변환 로직과 연동한다 */}
<NumericPropertyControl
label="라디오 텍스트 크기 (px)"
@@ -475,7 +475,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
<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"
className="w-full 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"
value={radioProps.widthMode ?? "full"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -574,7 +574,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
value={
radioProps.textColorCustom && radioProps.textColorCustom.trim() !== ""
? radioProps.textColorCustom
: "#f9fafb"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
@@ -595,7 +595,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
value={
radioProps.fillColorCustom && radioProps.fillColorCustom.trim() !== ""
? radioProps.fillColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
@@ -616,7 +616,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
value={
radioProps.strokeColorCustom && radioProps.strokeColorCustom.trim() !== ""
? radioProps.strokeColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
@@ -18,12 +18,12 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
return (
<div className="space-y-3 text-xs">
<h3 className="text-[11px] font-semibold text-slate-200"> </h3>
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h3>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full 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"
value={selectProps.labelMode ?? "text"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -37,9 +37,9 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full 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"
value={labelDisplay}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -53,10 +53,10 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
</label>
{labelDisplay === "visible" && (
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
<label className="flex flex-col gap-1 text-[11px] text-slate-500 dark: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"
className="w-full 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"
value={selectProps.labelLayout ?? "stacked"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -93,9 +93,9 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
)}
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={selectProps.label ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -106,9 +106,9 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark: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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={selectProps.formFieldName ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -120,10 +120,10 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-slate-400"></span>
<span className="text-slate-500 dark:text-slate-400"></span>
<button
type="button"
className="rounded border border-slate-700 bg-slate-900 px-2 py-0.5 text-[10px] text-slate-200 hover:bg-slate-800"
className="rounded border border-slate-300 bg-white px-2 py-0.5 text-[10px] text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={() => {
const next = Array.isArray((selectProps as any).options)
? [...(selectProps as any).options]
@@ -143,9 +143,9 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
<div className="space-y-2">
{(((selectProps as any).options ?? []) as any[]).map((opt, index) => (
<div key={opt.value ?? index} className="space-y-1 rounded border border-slate-800 p-2">
<div className="flex items-center gap-1">
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] items-center gap-1">
<input
className="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="flex-1 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"
placeholder="라벨"
value={opt.label ?? ""}
onChange={(e) => {
@@ -160,7 +160,7 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
}}
/>
<input
className="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="flex-1 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"
placeholder="값(value)"
value={opt.value ?? ""}
onChange={(e) => {
@@ -176,7 +176,7 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
/>
<button
type="button"
className="h-7 w-7 rounded border border-slate-700 bg-slate-950 text-[10px] text-slate-300 hover:bg-red-900/60 hover:border-red-700"
className="h-7 w-7 rounded border border-slate-300 bg-white text-[10px] text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-red-900/60 dark:hover:border-red-700"
onClick={() => {
const next = (((selectProps as any).options ?? []) as any[]).filter((_, i) => i !== index);
updateBlock(selectedBlockId, {
@@ -196,7 +196,7 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
<span className="text-slate-500"> .</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>
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h4>
<NumericPropertyControl
label="셀렉트 텍스트 크기 (px)"
unitLabel="(px)"
@@ -270,10 +270,10 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
} as any);
}}
/>
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
<label className="flex flex-col gap-1 text-[11px] text-slate-500 dark: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"
className="w-full 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"
value={selectProps.widthMode ?? "full"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -353,7 +353,7 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
value={
selectProps.textColorCustom && selectProps.textColorCustom.trim() !== ""
? selectProps.textColorCustom
: "#f9fafb"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
@@ -374,7 +374,7 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
value={
selectProps.fillColorCustom && selectProps.fillColorCustom.trim() !== ""
? selectProps.fillColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
@@ -395,7 +395,7 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
value={
selectProps.strokeColorCustom && selectProps.strokeColorCustom.trim() !== ""
? selectProps.strokeColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
+44 -37
View File
@@ -1070,8 +1070,8 @@ function EditorPageInner() {
});
return (
<main className="h-screen flex flex-col overflow-hidden">
<header className="border-b border-slate-800 px-6 py-4 flex items-center justify-between relative z-20 bg-slate-950/80 backdrop-blur">
<main className="h-screen flex flex-col overflow-hidden bg-slate-100 text-slate-900 dark:bg-slate-950 dark:text-slate-50">
<header className="border-b border-slate-200 px-6 py-4 flex items-center justify-between relative z-20 bg-white/80 backdrop-blur dark:border-slate-800 dark:bg-slate-950/80">
<div className="flex items-center gap-3">
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-slate-900 border border-sky-700 shadow-sm">
<Pencil className="w-4 h-4 text-sky-400" aria-hidden="true" />
@@ -1233,12 +1233,12 @@ function EditorPageInner() {
{activeModal === "project" && (
<div className="fixed inset-0 z-30 flex items-center justify-center bg-black/60">
<div className="w-full max-w-md rounded-lg border border-slate-700 bg-slate-900 p-4 text-xs text-slate-100 shadow-xl">
<div className="w-full max-w-md rounded-lg border border-slate-200 bg-white p-4 text-xs text-slate-900 shadow-xl dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium"> / </h3>
<button
type="button"
className="text-slate-400 hover:text-slate-100 text-sm"
className="text-slate-400 hover:text-slate-700 text-sm dark:hover:text-slate-100"
onClick={() => setActiveModal(null)}
>
@@ -1246,17 +1246,17 @@ function EditorPageInner() {
</div>
<div className="space-y-2">
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-600 dark: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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={projectConfig.title ?? ""}
onChange={(e) => updateProjectConfig({ title: e.target.value })}
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> (: my-landing)</span>
<span className="text-slate-600 dark:text-slate-400"> (: my-landing)</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"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={projectConfig.slug ?? ""}
onChange={(e) => updateProjectConfig({ slug: e.target.value })}
/>
@@ -1264,21 +1264,21 @@ function EditorPageInner() {
<div className="flex gap-2">
<button
type="button"
className="flex-1 rounded border border-sky-700 bg-sky-950 px-2 py-1 text-sky-100 hover:bg-sky-900"
className="flex-1 rounded border border-sky-500 bg-sky-50 px-2 py-1 text-sky-700 hover:bg-sky-100 dark:border-sky-700 dark:bg-sky-950 dark:text-sky-100 dark:hover:bg-sky-900"
onClick={handleSaveProject}
>
( + )
</button>
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-slate-100 hover:bg-slate-800"
className="flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleLoadProject}
>
</button>
</div>
{projectMessage && (
<p className="text-[11px] text-slate-300 mt-1">{projectMessage}</p>
<p className="text-[11px] text-slate-600 mt-1 dark:text-slate-300">{projectMessage}</p>
)}
</div>
</div>
@@ -1289,12 +1289,17 @@ function EditorPageInner() {
{activeModal === "json" && (
<div className="fixed inset-0 z-30 flex items-center justify-center bg-black/60">
<div className="w-full max-w-2xl rounded-lg border border-slate-700 bg-slate-900 p-4 text-xs text-slate-100 shadow-xl">
<div className="w-full max-w-2xl rounded-lg border border-slate-200 bg-white p-4 text-xs text-slate-900 shadow-xl dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium">JSON Export / Import <span className="text-xs text-slate-400">* .</span></h3>
<h3 className="text-sm font-medium">
JSON Export / Import
<span className="text-xs text-slate-500 dark:text-slate-400">
* .
</span>
</h3>
<button
type="button"
className="text-slate-400 hover:text-slate-100 text-sm"
className="text-slate-400 hover:text-slate-700 text-sm dark:hover:text-slate-100"
onClick={() => setActiveModal(null)}
>
@@ -1304,14 +1309,14 @@ function EditorPageInner() {
<div className="flex gap-2 mb-1">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 hover:bg-slate-800"
className="flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleExportJson}
>
JSON
</button>
<button
type="button"
className="rounded border border-red-700 bg-red-950 px-2 py-1 text-red-100 hover:bg-red-900"
className="rounded border border-red-300 bg-white px-2 py-1 text-red-700 hover:bg-red-50 dark:border-red-700 dark:bg-red-950 dark:text-red-100 dark:hover:bg-red-900"
onClick={handleClearCanvas}
>
@@ -1319,23 +1324,23 @@ function EditorPageInner() {
</div>
<div className="grid grid-cols-2 gap-3">
<label className="flex flex-col gap-1">
<span className="text-slate-400"> JSON</span>
<span className="text-slate-600 dark:text-slate-400"> JSON</span>
<textarea
className="w-full h-48 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[10px] font-mono outline-none focus:border-sky-500"
className="w-full h-48 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] font-mono text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={exportJson}
readOnly
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400">JSON에서 </span>
<span className="text-slate-600 dark:text-slate-400">JSON에서 </span>
<textarea
className="w-full h-48 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[10px] font-mono outline-none focus:border-sky-500"
className="w-full h-48 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] font-mono text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={importJson}
onChange={(e) => setImportJson(e.target.value)}
/>
<button
type="button"
className="mt-1 w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 hover:bg-slate-800"
className="mt-1 w-full rounded border border-slate-300 bg-white px-2 py-1 text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleApplyImportJson}
>
JSON
@@ -1509,8 +1514,10 @@ function SortableEditorBlock({
data-selected={isSelected ? "true" : "false"}
aria-selected={isSelected ? "true" : "false"}
className={`rounded border px-3 py-2 cursor-text transition-colors ${alignClass} ${sizeClass} ${leadingClass} ${weightClass} ${colorClass} ${maxWidthClass} ${decorationClass} ${
isSelected ? "border-sky-500 bg-slate-900/80" : "border-slate-700 bg-slate-900"
}`}
isSelected
? "border-sky-500 bg-sky-50 text-slate-900 dark:bg-slate-900/80 dark:text-slate-100"
: "border-slate-200 bg-white text-slate-900 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
}`}
onClick={(event) => {
// 멀티 선택(MVP): Cmd/Ctrl+클릭 시 선택 토글, 그 외에는 단일 선택으로 전환한다.
const isMeta = event.metaKey || event.ctrlKey;
@@ -1541,7 +1548,7 @@ function SortableEditorBlock({
<div className="flex items-start gap-2">
<button
type="button"
className="mt-0.5 h-4 w-4 rounded border border-slate-700 bg-slate-900 text-[10px] flex items-center justify-center text-slate-400 hover:bg-slate-800"
className="mt-0.5 h-4 w-4 rounded border border-slate-300 bg-slate-50 text-[10px] flex items-center justify-center text-slate-500 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400 dark:hover:bg-slate-800"
aria-label="블록 드래그 핸들"
{...listeners}
{...attributes}
@@ -1610,7 +1617,7 @@ function SortableEditorBlock({
className="h-4 w-auto"
/>
) : (
<span className="text-slate-200 shrink-0">{inputProps.label}</span>
<span className="shrink-0">{inputProps.label}</span>
)
)}
{(() => {
@@ -1715,7 +1722,7 @@ function SortableEditorBlock({
className="h-4 w-auto"
/>
) : (
<span className="text-slate-200">{selectProps.label}</span>
<span>{selectProps.label}</span>
)}
<div className="w-full">
<select
@@ -1792,14 +1799,14 @@ function SortableEditorBlock({
className="inline-block max-w-full h-auto"
/>
) : (
<span className="text-slate-200">{radioProps.groupLabel}</span>
<span>{radioProps.groupLabel}</span>
)}
<div style={optionsStyle} className={optionsLayoutClass}>
{options.map((opt) => (
<label key={opt.value} className="pb-form-option">
<input
type="radio"
className="h-4 w-4 border-slate-700 bg-slate-900 text-sky-500"
className="h-4 w-4"
name={radioProps.formFieldName}
value={opt.value}
readOnly
@@ -1863,8 +1870,8 @@ function SortableEditorBlock({
: { rowGap: `${optionGapPx}px` }
: {};
const groupContainerClassName = isInlineLayout
? "flex flex-row items-center text-xs text-slate-200"
: "flex flex-col gap-1 text-xs text-slate-200";
? "flex flex-row items-center text-xs"
: "flex flex-col gap-1 text-xs";
return (
<div className={groupContainerClassName} style={groupContainerStyle}>
@@ -1877,14 +1884,14 @@ function SortableEditorBlock({
className="inline-block max-w-full h-auto"
/>
) : (
<span className="text-slate-200">{checkboxProps.groupLabel}</span>
<span>{checkboxProps.groupLabel}</span>
)}
<div className={optionsLayoutClass} style={optionsStyle}>
{options.map((opt) => (
<label key={opt.value} className="pb-form-option">
<input
type="checkbox"
className="h-4 w-4 rounded border-slate-700 bg-slate-900 text-sky-500"
className="h-4 w-4 rounded"
name={checkboxProps.formFieldName}
value={opt.value}
readOnly
@@ -2050,7 +2057,7 @@ function SortableEditorBlock({
return (
<div
className={`w-full border border-dashed border-slate-700 bg-slate-900/40 rounded flex items-center overflow-hidden ${alignClass}`}
className={`w-full border border-dashed border-slate-700 bg-slate-500/40 rounded flex items-center overflow-hidden ${alignClass}`}
>
{hasSrc ? (
// eslint-disable-next-line @next/next/no-img-element
@@ -2061,7 +2068,7 @@ function SortableEditorBlock({
style={{ ...containerStyle, ...imageStyle }}
/>
) : (
<span className="text-[10px] text-slate-500 px-2 py-4">
<span className="text-[10px] text-slate-900 dark:text-slate-500 px-2 py-4">
URL .
</span>
)}
@@ -2162,7 +2169,7 @@ function SortableEditorBlock({
style={index < nodes.length - 1 ? { marginBottom: gapPx } : undefined}
>
<span className="break-words align-middle">{node.text}</span>
<span className="ml-2 inline-flex items-center gap-1 text-[10px] text-slate-300 align-middle">
<span className="ml-2 inline-flex items-center gap-1 text-[10px] text-slate-900 dark:text-slate-300 align-middle">
<button
type="button"
className="rounded border border-slate-700 px-1 py-0.5 hover:bg-slate-800"
@@ -2367,13 +2374,13 @@ function ColumnDroppable({ id, sectionId, columnId, basis, blocks, renderBlocks,
style={{ flexBasis: basis }}
>
<div
className={`border rounded flex flex-col gap-2 items-stretch justify-start px-2 py-2 bg-slate-950/40 ${
className={`border rounded flex flex-col gap-2 items-stretch justify-start px-2 py-2 bg-slate-100/40 dark:bg-slate-950/40 ${
isActiveColumn ? "border-sky-500" : "border-slate-800/80"
}`}
style={{}}
>
{blocks.length === 0 ? (
<span className="text-[11px] px-2 text-center text-slate-500">
<span className="text-[11px] px-2 text-center text-slate-900 dark:text-slate-300">
{`컬럼 영역 (span ${span}/12)`}
</span>
) : (
+52 -49
View File
@@ -90,8 +90,11 @@ export function BlocksSidebar() {
const [isTemplatesOpen, setIsTemplatesOpen] = useState(true);
return (
<aside className="w-60 border-r border-slate-800 p-4 text-sm space-y-3 overflow-y-auto pb-scroll bg-slate-950/40">
<h2 className="font-medium flex items-center gap-2 text-slate-200">
<aside
data-testid="blocks-sidebar"
className="w-60 border-r border-slate-200 bg-white p-4 text-sm space-y-3 overflow-y-auto pb-scroll dark:border-slate-800 dark:bg-slate-950/40"
>
<h2 className="font-medium flex items-center gap-2 text-slate-900 dark:text-slate-100">
<button
type="button"
className="flex items-center justify-between w-full text-left"
@@ -108,7 +111,7 @@ export function BlocksSidebar() {
<>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="w-full rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddText}
>
<span className="inline-flex items-center gap-2">
@@ -118,7 +121,7 @@ export function BlocksSidebar() {
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="w-full rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddButton}
>
<span className="inline-flex items-center gap-2">
@@ -128,7 +131,7 @@ export function BlocksSidebar() {
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="w-full rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddImage}
>
<span className="inline-flex items-center gap-2">
@@ -138,7 +141,7 @@ export function BlocksSidebar() {
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="w-full rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddVideo}
>
<span className="inline-flex items-center gap-2">
@@ -148,7 +151,7 @@ export function BlocksSidebar() {
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="w-full rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddDivider}
>
<span className="inline-flex items-center gap-2">
@@ -158,7 +161,7 @@ export function BlocksSidebar() {
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="w-full rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddList}
>
<span className="inline-flex items-center gap-2">
@@ -168,7 +171,7 @@ export function BlocksSidebar() {
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="w-full rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddSection}
>
<span className="inline-flex items-center gap-2">
@@ -179,8 +182,8 @@ export function BlocksSidebar() {
</>
)}
<div className="pt-3 border-t border-slate-800 mt-3 space-y-2">
<h3 className="text-[11px] font-medium text-slate-300 flex items-center gap-2">
<div className="pt-3 border-t border-slate-200 mt-3 space-y-2 dark:border-slate-800">
<h3 className="text-[11px] font-medium text-slate-800 flex items-center gap-2 dark:text-slate-200">
<button
type="button"
className="flex items-center justify-between w-full text-left"
@@ -197,7 +200,7 @@ export function BlocksSidebar() {
<>
<button
type="button"
className="w-full rounded border border-emerald-700 bg-emerald-950 px-3 py-2 text-left text-xs text-emerald-100 hover:bg-emerald-900"
className="w-full rounded border border-emerald-200 bg-emerald-50 px-3 py-2 text-left text-xs text-emerald-900 hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-100 dark:hover:bg-emerald-900"
onClick={handleAddFormBlock}
>
<span className="inline-flex items-center gap-2">
@@ -207,7 +210,7 @@ export function BlocksSidebar() {
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="w-full rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddFormInput}
>
<span className="inline-flex items-center gap-2">
@@ -217,7 +220,7 @@ export function BlocksSidebar() {
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="w-full rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddFormSelect}
>
<span className="inline-flex items-center gap-2">
@@ -227,7 +230,7 @@ export function BlocksSidebar() {
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="w-full rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddFormRadio}
>
<span className="inline-flex items-center gap-2">
@@ -237,7 +240,7 @@ export function BlocksSidebar() {
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="w-full rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddFormCheckbox}
>
<span className="inline-flex items-center gap-2">
@@ -249,8 +252,8 @@ export function BlocksSidebar() {
)}
</div>
<div className="pt-3 border-t border-slate-800 mt-3 space-y-3">
<h3 className="text-[11px] font-medium text-slate-300 flex items-center gap-2">
<div className="pt-3 border-t border-slate-200 mt-3 space-y-3 dark:border-slate-800">
<h3 className="text-[11px] font-medium text-slate-800 flex items-center gap-2 dark:text-slate-200">
<button
type="button"
className="flex items-center justify-between w-full text-left"
@@ -267,20 +270,20 @@ export function BlocksSidebar() {
{isTemplatesOpen && (
<div className="space-y-3">
<div className="space-y-2">
<p className="text-[10px] font-semibold text-slate-400"> · CTA</p>
<p className="text-[10px] font-semibold text-slate-600 dark:text-slate-400"> · CTA</p>
<div className="space-y-2">
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="rounded border border-slate-200 bg-slate-50 p-2 space-y-1 dark:border-slate-800 dark:bg-slate-950/50">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="flex-1 rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddHeroTemplate}
>
Hero 릿
</button>
<div
data-testid="template-preview-hero"
className="shrink-0 flex h-6 w-10 rounded border border-slate-700 bg-slate-900 p-[2px]"
className="shrink-0 flex h-6 w-10 rounded border border-slate-300 bg-slate-100 p-[2px] dark:border-slate-700 dark:bg-slate-900"
>
<div className="flex h-full w-full flex-col items-center justify-center gap-[2px]">
<div className="h-[2px] w-3/4 bg-slate-700/70" />
@@ -294,18 +297,18 @@ export function BlocksSidebar() {
</p>
</div>
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="rounded border border-slate-200 bg-slate-50 p-2 space-y-1 dark:border-slate-800 dark:bg-slate-950/50">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="flex-1 rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddCtaTemplate}
>
CTA 릿
</button>
<div
data-testid="template-preview-cta"
className="shrink-0 flex h-6 w-10 rounded border border-slate-700 bg-slate-900 p-[2px]"
className="shrink-0 flex h-6 w-10 rounded border border-slate-300 bg-slate-100 p-[2px] dark:border-slate-700 dark:bg-slate-900"
>
<div className="flex h-full w-full items-center gap-[2px]">
<div className="flex-1 flex flex-col justify-center gap-[1px]">
@@ -322,20 +325,20 @@ export function BlocksSidebar() {
</div>
<div className="space-y-2">
<p className="text-[10px] font-semibold text-slate-400"> </p>
<p className="text-[10px] font-semibold text-slate-600 dark:text-slate-400"> </p>
<div className="space-y-2">
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="rounded border border-slate-200 bg-slate-50 p-2 space-y-1 dark:border-slate-800 dark:bg-slate-950/50">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="flex-1 rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddFeaturesTemplate}
>
릿
</button>
<div
data-testid="template-preview-features"
className="shrink-0 flex h-6 w-10 items-stretch gap-[2px] rounded border border-slate-700 bg-slate-900 p-[2px]"
className="shrink-0 flex h-6 w-10 items-stretch gap-[2px] rounded border border-slate-300 bg-slate-100 p-[2px] dark:border-slate-700 dark:bg-slate-900"
>
<div className="flex-1 flex flex-col gap-[1px]">
<div className="h-[2px] w-full bg-slate-700/70" />
@@ -354,18 +357,18 @@ export function BlocksSidebar() {
<p className="text-[10px] text-slate-400 leading-snug">3 </p>
</div>
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="rounded border border-slate-200 bg-slate-50 p-2 space-y-1 dark:border-slate-800 dark:bg-slate-950/50">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="flex-1 rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddFaqTemplate}
>
FAQ 릿
</button>
<div
data-testid="template-preview-faq"
className="shrink-0 flex h-6 w-10 items-start gap-[2px] rounded border border-slate-700 bg-slate-900 p-[2px]"
className="shrink-0 flex h-6 w-10 items-start gap-[2px] rounded border border-slate-300 bg-slate-100 p-[2px] dark:border-slate-700 dark:bg-slate-900"
>
<div className="w-2 flex flex-col gap-[1px]">
<div className="h-[2px] w-full bg-slate-700/70" />
@@ -380,18 +383,18 @@ export function BlocksSidebar() {
<p className="text-[10px] text-slate-400 leading-snug"> (FAQ) </p>
</div>
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="rounded border border-slate-200 bg-slate-50 p-2 space-y-1 dark:border-slate-800 dark:bg-slate-950/50">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="flex-1 rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddPricingTemplate}
>
릿
</button>
<div
data-testid="template-preview-pricing"
className="shrink-0 flex h-6 w-10 items-stretch gap-[2px] rounded border border-slate-700 bg-slate-900 p-[2px]"
className="shrink-0 flex h-6 w-10 items-stretch gap-[2px] rounded border border-slate-300 bg-slate-100 p-[2px] dark:border-slate-700 dark:bg-slate-900"
>
<div className="flex-1 flex flex-col items-center justify-end gap-[1px] border border-slate-700/30 rounded-[1px]">
<div className="h-[1px] w-1/2 bg-slate-700/70" />
@@ -411,18 +414,18 @@ export function BlocksSidebar() {
<p className="text-[10px] text-slate-400 leading-snug">/ </p>
</div>
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="rounded border border-slate-200 bg-slate-50 p-2 space-y-1 dark:border-slate-800 dark:bg-slate-950/50">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="flex-1 rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddBlogTemplate}
>
릿
</button>
<div
data-testid="template-preview-blog"
className="shrink-0 flex h-6 w-10 items-stretch gap-[2px] rounded border border-slate-700 bg-slate-900 p-[2px]"
className="shrink-0 flex h-6 w-10 items-stretch gap-[2px] rounded border border-slate-300 bg-slate-100 p-[2px] dark:border-slate-700 dark:bg-slate-900"
>
<div className="flex-1 flex flex-col gap-[1px]">
<div className="h-2 w-full bg-slate-600/50 rounded-[1px]" />
@@ -447,20 +450,20 @@ export function BlocksSidebar() {
</div>
<div className="space-y-2">
<p className="text-[10px] font-semibold text-slate-400">/</p>
<p className="text-[10px] font-semibold text-slate-600 dark:text-slate-400">/</p>
<div className="space-y-2">
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="rounded border border-slate-200 bg-slate-50 p-2 space-y-1 dark:border-slate-800 dark:bg-slate-950/50">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="flex-1 rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddTestimonialsTemplate}
>
릿
</button>
<div
data-testid="template-preview-testimonials"
className="shrink-0 flex h-6 w-10 items-stretch gap-[2px] rounded border border-slate-700 bg-slate-900 p-[2px]"
className="shrink-0 flex h-6 w-10 items-stretch gap-[2px] rounded border border-slate-300 bg-slate-100 p-[2px] dark:border-slate-700 dark:bg-slate-900"
>
<div className="flex-1 flex flex-col justify-between border border-slate-700/30 rounded-[1px] p-[1px]">
<div className="h-[1px] w-full bg-slate-700/50" />
@@ -479,18 +482,18 @@ export function BlocksSidebar() {
<p className="text-[10px] text-slate-400 leading-snug"> (Testimonials) </p>
</div>
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="rounded border border-slate-200 bg-slate-50 p-2 space-y-1 dark:border-slate-800 dark:bg-slate-950/50">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="flex-1 rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddTeamTemplate}
>
Team 릿
</button>
<div
data-testid="template-preview-team"
className="shrink-0 flex h-6 w-10 items-stretch gap-[2px] rounded border border-slate-700 bg-slate-900 p-[2px]"
className="shrink-0 flex h-6 w-10 items-stretch gap-[2px] rounded border border-slate-300 bg-slate-100 p-[2px] dark:border-slate-700 dark:bg-slate-900"
>
<div className="flex-1 flex flex-col items-center gap-[1px]">
<div className="h-2 w-2 rounded-full bg-slate-600/80" />
@@ -515,20 +518,20 @@ export function BlocksSidebar() {
</div>
<div className="space-y-2">
<p className="text-[10px] font-semibold text-slate-400">/</p>
<p className="text-[10px] font-semibold text-slate-600 dark:text-slate-400">/</p>
<div className="space-y-2">
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="rounded border border-slate-200 bg-slate-50 p-2 space-y-1 dark:border-slate-800 dark:bg-slate-950/50">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
className="flex-1 rounded border border-slate-200 bg-slate-50 px-3 py-2 text-left text-xs text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={handleAddFooterTemplate}
>
Footer 릿
</button>
<div
data-testid="template-preview-footer"
className="shrink-0 flex h-6 w-10 items-center justify-between gap-[2px] rounded border border-slate-700 bg-slate-900 p-[2px]"
className="shrink-0 flex h-6 w-10 items-center justify-between gap-[2px] rounded border border-slate-300 bg-slate-100 p-[2px] dark:border-slate-700 dark:bg-slate-900"
>
<div className="flex-1 h-[2px] bg-slate-700/70" />
<div className="flex-1 flex flex-col gap-[1px]">
+16 -16
View File
@@ -24,7 +24,7 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span> </span>
<textarea
className="w-full min-h-[60px] rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full min-h-[60px] rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="버튼 텍스트"
value={buttonProps.label}
onChange={(e) => {
@@ -34,13 +34,13 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc
</label>
</div>
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3 text-xs text-slate-400">
<h4 className="text-[11px] font-semibold text-slate-200"> </h4>
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h4>
<div className="space-y-1">
<label className="flex flex-col gap-1">
<span> </span>
<select
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="버튼 이미지 소스"
value={imageSource}
onChange={(e) => {
@@ -74,7 +74,7 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc
<label className="flex flex-col gap-1">
<span> URL</span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="버튼 이미지 URL"
value={buttonProps.imageSrc ?? ""}
onChange={(e) => {
@@ -142,7 +142,7 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc
<label className="flex flex-col gap-1">
<span> </span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="버튼 이미지 대체 텍스트"
value={buttonProps.imageAlt ?? ""}
onChange={(e) => {
@@ -156,7 +156,7 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc
<label className="flex flex-col gap-1">
<span> </span>
<select
className="rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="버튼 이미지 위치"
value={buttonProps.imagePlacement ?? "left"}
onChange={(e) => {
@@ -223,7 +223,7 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span> </span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="버튼 링크"
value={buttonProps.href}
onChange={(e) => {
@@ -236,7 +236,7 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span></span>
<select
className="rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="버튼 정렬"
value={buttonProps.align ?? "left"}
onChange={(e) => {
@@ -259,9 +259,9 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc
ariaLabelColorInput="버튼 텍스트 색상 피커"
ariaLabelHexInput="버튼 텍스트 색상 HEX"
value={
buttonProps.textColorCustom && buttonProps.textColorCustom.startsWith("#")
buttonProps.textColorCustom && buttonProps.textColorCustom.trim() !== ""
? buttonProps.textColorCustom
: "#f9fafb"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
@@ -280,7 +280,7 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span></span>
<select
className="rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="버튼 스타일"
value={buttonProps.variant ?? "solid"}
onChange={(e) => {
@@ -300,9 +300,9 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc
ariaLabelColorInput="버튼 채움 색상 피커"
ariaLabelHexInput="버튼 채움 색상 HEX"
value={
buttonProps.fillColorCustom && buttonProps.fillColorCustom.startsWith("#")
buttonProps.fillColorCustom && buttonProps.fillColorCustom.trim() !== ""
? buttonProps.fillColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
@@ -323,9 +323,9 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc
ariaLabelColorInput="버튼 외곽선 색상 피커"
ariaLabelHexInput="버튼 외곽선 색상 HEX"
value={
buttonProps.strokeColorCustom && buttonProps.strokeColorCustom.startsWith("#")
buttonProps.strokeColorCustom && buttonProps.strokeColorCustom.trim() !== ""
? buttonProps.strokeColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
@@ -368,7 +368,7 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span> </span>
<select
className="rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="버튼 너비 모드"
value={buttonProps.widthMode ?? (buttonProps.fullWidth ? "full" : "auto")}
onChange={(e) => {
@@ -17,7 +17,7 @@ export function DividerPropertiesPanel({ dividerProps, selectedBlockId, updateBl
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span></span>
<select
className="rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="구분선 정렬"
value={dividerProps.align}
onChange={(e) => {
@@ -35,7 +35,7 @@ export function DividerPropertiesPanel({ dividerProps, selectedBlockId, updateBl
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span></span>
<select
className="rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="구분선 두께"
value={dividerProps.thickness}
onChange={(e) => {
@@ -50,13 +50,13 @@ export function DividerPropertiesPanel({ dividerProps, selectedBlockId, updateBl
</div>
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3 text-xs text-slate-400">
<h4 className="text-[11px] font-semibold text-slate-200"> </h4>
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h4>
{/* 길이/너비 모드 */}
<label className="flex flex-col gap-1">
<span> </span>
<select
className="rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="구분선 길이 모드"
value={dividerProps.widthMode ?? "full"}
onChange={(e) => {
@@ -97,7 +97,7 @@ export function DividerPropertiesPanel({ dividerProps, selectedBlockId, updateBl
value={
dividerProps.colorHex && dividerProps.colorHex.trim() !== ""
? dividerProps.colorHex
: TEXT_COLOR_PALETTE[0]?.color ?? "#64748b"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, { colorHex: hex } as any);
@@ -22,7 +22,7 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
<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-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="이미지 소스"
value={source}
onChange={(e) => {
@@ -50,7 +50,7 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span> URL</span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="이미지 URL"
value={imageProps.src}
onChange={(e) => {
@@ -115,7 +115,7 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span> </span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="대체 텍스트"
value={imageProps.alt}
onChange={(e) => {
@@ -126,7 +126,7 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
</div>
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3 text-xs text-slate-400">
<h4 className="text-[11px] font-semibold text-slate-200"> </h4>
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h4>
{/* 카드 배경색 */}
<div className="space-y-1">
@@ -147,7 +147,7 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
<label className="flex flex-col gap-1">
<span></span>
<select
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="이미지 정렬"
value={imageProps.align ?? "center"}
onChange={(e) =>
@@ -166,7 +166,7 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
<label className="flex flex-col gap-1">
<span> </span>
<select
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="이미지 너비 모드"
value={imageProps.widthMode ?? "auto"}
onChange={(e) =>
@@ -30,7 +30,7 @@ export function ListPropertiesPanel({
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span> ( )</span>
<textarea
className="w-full min-h-[80px] rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full min-h-[80px] rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="리스트 아이템들"
value={(() => {
const tree = (listProps as any).itemsTree as any[] | undefined;
@@ -85,7 +85,7 @@ export function ListPropertiesPanel({
<label className="flex items-center gap-2">
<span></span>
<select
className="rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="리스트 정렬"
value={listProps.align}
onChange={(e) => {
@@ -101,7 +101,7 @@ export function ListPropertiesPanel({
</div>
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3 text-xs text-slate-400">
<h4 className="text-[11px] font-semibold text-slate-200"> </h4>
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h4>
{/* 글자 크기 */}
<NumericPropertyControl
@@ -153,10 +153,12 @@ export function ListPropertiesPanel({
label="텍스트 색상"
ariaLabelColorInput="리스트 텍스트 색상 피커"
ariaLabelHexInput="리스트 텍스트 색상 HEX"
// textColorCustom 이 비어 있으면 커스텀 색상을 사용하지 않고, "없음" 상태로 취급한다.
// 이 경우 HEX 인풋은 빈 문자열을 유지하고, 팔레트 라벨은 "없음"으로 표시된다.
value={
listProps.textColorCustom && listProps.textColorCustom.trim() !== ""
? listProps.textColorCustom
: "#e5e7eb"
: ""
}
onChange={(hex) => {
updateBlock(selectedBlockId, { textColorCustom: hex } as any);
@@ -182,7 +184,7 @@ export function ListPropertiesPanel({
<label className="flex flex-col gap-1">
<span>릿 </span>
<select
className="rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="리스트 불릿 스타일"
value={listProps.bulletStyle ?? "disc"}
onChange={(e) => {
@@ -38,14 +38,14 @@ export function ProjectPropertiesPanel() {
};
return (
<div className="space-y-4 text-xs text-slate-200">
<div className="space-y-4 text-xs text-slate-900 dark:text-slate-200">
<h3 className="text-sm font-medium text-slate-100"> </h3>
<div className="space-y-1">
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark:text-slate-400"> </span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="프로젝트 제목"
value={projectConfig.title}
onChange={(e) => updateProjectConfig({ title: e.target.value })}
@@ -55,9 +55,9 @@ export function ProjectPropertiesPanel() {
<div className="space-y-1">
<label className="flex flex-col gap-1">
<span className="text-slate-400"> (slug)</span>
<span className="text-slate-500 dark:text-slate-400"> (slug)</span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="프로젝트 주소 (slug)"
value={projectConfig.slug}
onChange={(e) => updateProjectConfig({ slug: e.target.value })}
@@ -105,12 +105,12 @@ export function ProjectPropertiesPanel() {
</div>
<div className="space-y-2 border-t border-slate-800 pt-3">
<h4 className="text-[11px] font-semibold text-slate-200">SEO / </h4>
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200">SEO / </h4>
<label className="flex flex-col gap-1">
<span className="text-slate-400">SEO </span>
<span className="text-slate-500 dark:text-slate-400">SEO </span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="SEO 타이틀"
placeholder={projectConfig.title || "페이지 제목"}
value={projectConfig.seoTitle ?? ""}
@@ -119,9 +119,9 @@ export function ProjectPropertiesPanel() {
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark:text-slate-400"> </span>
<textarea
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500 min-h-[56px]"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-[11px] text-slate-900 outline-none focus:border-sky-500 min-h-[56px] dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="메타 디스크립션"
placeholder="검색엔진 및 SNS 공유에 노출될 페이지 설명을 입력하세요."
value={projectConfig.seoDescription ?? ""}
@@ -130,9 +130,9 @@ export function ProjectPropertiesPanel() {
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400">OG/Twitter URL</span>
<span className="text-slate-500 dark:text-slate-400">OG/Twitter URL</span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="OG/Twitter 이미지 URL"
placeholder="예: https://example.com/og-image.png"
value={projectConfig.seoOgImageUrl ?? ""}
@@ -141,9 +141,9 @@ export function ProjectPropertiesPanel() {
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400">Canonical URL</span>
<span className="text-slate-500 dark:text-slate-400">Canonical URL</span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="Canonical URL"
placeholder="예: https://example.com/landing"
value={projectConfig.seoCanonicalUrl ?? ""}
@@ -151,10 +151,10 @@ export function ProjectPropertiesPanel() {
/>
</label>
<label className="flex items-center gap-2 text-[11px] text-slate-300">
<label className="flex items-center gap-2 text-[11px] text-slate-600 dark:text-slate-300">
<input
type="checkbox"
className="h-3 w-3 rounded border-slate-600 bg-slate-900"
className="h-3 w-3 rounded border-slate-300 bg-white text-sky-600 dark:border-slate-600 dark:bg-slate-900"
aria-label="검색 엔진에 노출하지 않기 (noindex)"
checked={Boolean(projectConfig.seoNoIndex)}
onChange={(e) => updateProjectConfig({ seoNoIndex: e.target.checked })}
@@ -165,9 +165,9 @@ export function ProjectPropertiesPanel() {
<div className="space-y-1">
<label className="flex flex-col gap-1">
<span className="text-slate-400"> head HTML</span>
<span className="text-slate-500 dark:text-slate-400"> head HTML</span>
<textarea
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] font-mono outline-none focus:border-sky-500 min-h-[72px]"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-[11px] font-mono text-slate-900 outline-none focus:border-sky-500 min-h-[72px] dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="페이지 head HTML"
value={projectConfig.headHtml ?? ""}
onChange={(e) => updateProjectConfig({ headHtml: e.target.value })}
@@ -178,9 +178,9 @@ export function ProjectPropertiesPanel() {
<div className="space-y-1">
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-500 dark:text-slate-400"> </span>
<textarea
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] font-mono outline-none focus:border-sky-500 min-h-[72px]"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-[11px] font-mono text-slate-900 outline-none focus:border-sky-500 min-h-[72px] dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="추적 스크립트"
value={projectConfig.trackingScript ?? ""}
onChange={(e) => updateProjectConfig({ trackingScript: e.target.value })}
+6 -6
View File
@@ -666,13 +666,13 @@ export function PropertiesSidebar(props: PropertiesSidebarProps) {
return (
<aside
data-testid="properties-sidebar"
className="w-80 p-4 text-sm border-l border-slate-800 flex flex-col gap-4 overflow-auto pb-scroll bg-slate-950/40"
className="w-80 p-4 text-sm border-l border-slate-200 bg-white flex flex-col gap-4 overflow-auto pb-scroll dark:border-slate-800 dark:bg-slate-950/40"
onKeyDownCapture={(e) => {
// 속성 패널 안에서 발생한 키 입력은 에디터 단축키/텍스트 블록 편집으로 전달되지 않도록 막는다.
e.stopPropagation();
}}
>
<h2 className="font-medium mb-2 flex items-center gap-2 text-slate-200">
<h2 className="font-medium mb-2 flex items-center gap-2 text-slate-900 dark:text-slate-100">
<SlidersHorizontal className="w-4 h-4 text-sky-400" aria-hidden="true" />
<span> </span>
</h2>
@@ -681,7 +681,7 @@ export function PropertiesSidebar(props: PropertiesSidebarProps) {
<div className="flex gap-2 text-[11px]">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-2 py-1 text-slate-100 hover:bg-red-900/60 hover:border-red-700"
className="flex-1 rounded border border-slate-200 bg-slate-50 px-2 py-1 text-slate-900 hover:bg-red-50 hover:border-red-300 hover:text-red-700 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-red-900/60 dark:hover:border-red-700"
onClick={() => removeBlock(selectedBlockId)}
>
<span className="inline-flex items-center gap-2">
@@ -691,7 +691,7 @@ export function PropertiesSidebar(props: PropertiesSidebarProps) {
</button>
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-2 py-1 text-slate-100 hover:bg-slate-800"
className="flex-1 rounded border border-slate-200 bg-slate-50 px-2 py-1 text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={() => duplicateBlock(selectedBlockId)}
>
<span className="inline-flex items-center gap-2">
@@ -703,7 +703,7 @@ export function PropertiesSidebar(props: PropertiesSidebarProps) {
<div className="flex justify-end text-[11px]">
<button
type="button"
className="inline-flex items-center gap-1 rounded border border-slate-700 bg-slate-900 px-2 py-1 text-slate-100 hover:bg-slate-800"
className="inline-flex items-center gap-1 rounded border border-slate-200 bg-slate-50 px-2 py-1 text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={() => setHelpOpen(true)}
>
<HelpCircle className="w-3 h-3" aria-hidden="true" />
@@ -864,7 +864,7 @@ export function PropertiesSidebar(props: PropertiesSidebarProps) {
return (
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black/60">
<div className="w-full max-w-md rounded-lg border border-slate-700 bg-slate-900 p-4 text-xs text-slate-100 shadow-xl">
<div className="w-full max-w-md rounded-lg border border-slate-200 bg-white p-4 text-xs text-slate-900 shadow-xl dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium">{help.title}</h3>
<button
@@ -61,7 +61,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
<label className="flex flex-col gap-1">
<span> </span>
<select
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="배경 이미지 소스"
value={backgroundSource}
onChange={(e) => {
@@ -94,7 +94,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
<label className="flex flex-col gap-1">
<span> URL</span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="배경 이미지 URL"
value={sectionProps.backgroundImageSrc ?? ""}
onChange={(e) => {
@@ -157,7 +157,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
<label className="flex flex-col gap-1">
<span> </span>
<select
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="배경 이미지 위치 모드"
value={positionMode}
onChange={(e) =>
@@ -174,7 +174,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
<label className="flex flex-col gap-1">
<span> </span>
<select
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="배경 이미지 크기"
value={sectionProps.backgroundImageSize ?? "cover"}
onChange={(e) =>
@@ -193,7 +193,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
<label className="flex flex-col gap-1">
<span> </span>
<select
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="배경 이미지 위치"
value={sectionProps.backgroundImagePosition ?? "center"}
onChange={(e) =>
@@ -266,7 +266,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
<label className="flex flex-col gap-1">
<span> </span>
<select
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="배경 이미지 반복"
value={sectionProps.backgroundImageRepeat ?? "no-repeat"}
onChange={(e) =>
@@ -290,7 +290,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
<label className="flex flex-col gap-1">
<span> </span>
<select
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="배경 비디오 소스"
value={backgroundVideoSource}
onChange={(e) => {
@@ -323,7 +323,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
<label className="flex flex-col gap-1">
<span> URL</span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="배경 비디오 URL"
value={sectionProps.backgroundVideoSrc ?? ""}
onChange={(e) => {
@@ -413,7 +413,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
<label className="flex flex-col gap-1">
<span> </span>
<select
className="rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="섹션 컬럼 레이아웃"
value={(() => {
const cols = sectionProps.columns ?? [];
@@ -725,7 +725,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
<label className="flex flex-col gap-1">
<span> </span>
<select
className="rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="섹션 컬럼 세로 정렬"
value={sectionProps.alignItems ?? "top"}
onChange={(e) => {
@@ -99,7 +99,7 @@ export function TextPropertiesPanel({
<div className="space-y-1">
<p className="text-xs text-slate-400"> </p>
<textarea
className="w-full min-h-[80px] rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full min-h-[80px] rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="선택한 텍스트 블록 내용"
value={textProps.text}
onChange={(e) => {
@@ -116,7 +116,7 @@ export function TextPropertiesPanel({
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span></span>
<select
className="rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="정렬"
value={textProps.align}
onChange={(e) => {
@@ -140,7 +140,7 @@ export function TextPropertiesPanel({
className={`px-2 py-0.5 rounded border text-[11px] ${
textProps.underline
? "border-sky-500 bg-sky-900/40 text-sky-100"
: "border-slate-700 bg-slate-900 text-slate-300"
: "border-slate-300 bg-white text-slate-900 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
}`}
onClick={() => {
updateBlock(selectedBlockId, {
@@ -155,7 +155,7 @@ export function TextPropertiesPanel({
className={`px-2 py-0.5 rounded border text-[11px] ${
textProps.strike
? "border-sky-500 bg-sky-900/40 text-sky-100"
: "border-slate-700 bg-slate-900 text-slate-300"
: "border-slate-300 bg-white text-slate-900 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
}`}
onClick={() => {
updateBlock(selectedBlockId, {
@@ -170,7 +170,7 @@ export function TextPropertiesPanel({
className={`px-2 py-0.5 rounded border text-[11px] ${
textProps.italic
? "border-sky-500 bg-sky-900/40 text-sky-100"
: "border-slate-700 bg-slate-900 text-slate-300"
: "border-slate-300 bg-white text-slate-900 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
}`}
onClick={() => {
updateBlock(selectedBlockId, {
@@ -246,7 +246,7 @@ export function TextPropertiesPanel({
<span> </span>
<div className="flex items-center gap-2">
<select
className="w-32 rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
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="글자 간격 프리셋"
value={letterSpacingPreset}
onChange={(e) => {
@@ -457,7 +457,7 @@ export function TextPropertiesPanel({
<span> </span>
<div className="flex items-center gap-2">
<select
className="w-32 rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
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="최대 너비 프리셋"
value={maxWidthScale}
onChange={(e) => {
@@ -502,7 +502,7 @@ export function TextPropertiesPanel({
/>
</div>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="최대 너비 커스텀"
placeholder="예: 600px, 40rem, 80%, 60ch"
value={textProps.maxWidthCustom ?? ""}
@@ -24,7 +24,7 @@ export function VideoPropertiesPanel({ videoProps, selectedBlockId, updateBlock
<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-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="비디오 소스"
value={source}
onChange={(e) => {
@@ -53,7 +53,7 @@ export function VideoPropertiesPanel({ videoProps, selectedBlockId, updateBlock
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span> URL</span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="비디오 URL"
value={videoProps.sourceUrl ?? ""}
onChange={(e) => {
@@ -112,7 +112,7 @@ export function VideoPropertiesPanel({ videoProps, selectedBlockId, updateBlock
<label className="flex flex-col gap-1">
<span> URL</span>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="포스터 이미지 URL"
value={videoProps.posterImageSrc ?? ""}
onChange={(e) => {
@@ -175,7 +175,7 @@ export function VideoPropertiesPanel({ videoProps, selectedBlockId, updateBlock
)}
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3 text-xs text-slate-400">
<h4 className="text-[11px] font-semibold text-slate-200"> </h4>
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h4>
{/* 카드 배경색 */}
<div className="space-y-1">
@@ -196,7 +196,7 @@ export function VideoPropertiesPanel({ videoProps, selectedBlockId, updateBlock
<label className="flex flex-col gap-1">
<span> </span>
<select
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="비디오 정렬"
value={videoProps.align ?? "center"}
onChange={(e) =>
@@ -215,7 +215,7 @@ export function VideoPropertiesPanel({ videoProps, selectedBlockId, updateBlock
<label className="flex flex-col gap-1">
<span> </span>
<select
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="비디오 너비 모드"
value={videoProps.widthMode ?? "auto"}
onChange={(e) =>
@@ -311,7 +311,7 @@ export function VideoPropertiesPanel({ videoProps, selectedBlockId, updateBlock
<label className="flex flex-col gap-1">
<span> </span>
<select
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
className="w-full 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="화면 비율"
value={videoProps.aspectRatio ?? "16:9"}
onChange={(e) =>
+33 -9
View File
@@ -3,6 +3,7 @@
import { FormEvent, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { SunMoon } from "lucide-react";
// 로그인 페이지 컴포넌트
// - 이메일/비밀번호를 입력받아 /api/auth/login 으로 요청을 전송한다.
@@ -16,6 +17,19 @@ export default function LoginPage() {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleToggleTheme = () => {
if (typeof document === "undefined") {
return;
}
const root = document.documentElement;
if (!root) {
return;
}
root.classList.toggle("dark");
};
useEffect(() => {
let cancelled = false;
@@ -71,16 +85,26 @@ export default function LoginPage() {
};
return (
<main className="min-h-screen flex items-center justify-center bg-slate-950 text-slate-50">
<div className="w-full max-w-sm rounded-lg border border-slate-800 bg-slate-900/70 p-6 shadow-xl">
<h1 className="text-lg font-semibold mb-1"></h1>
<p className="text-xs text-slate-400 mb-4"> .</p>
<main className="min-h-screen flex items-center justify-center bg-slate-100 text-slate-900 dark:bg-slate-950 dark:text-slate-50">
<div className="w-full max-w-sm rounded-lg border border-slate-200 bg-white p-6 shadow-xl dark:border-slate-800 dark:bg-slate-900/70">
<div className="flex items-center justify-between mb-1">
<h1 className="text-lg font-semibold"></h1>
<button
type="button"
className="inline-flex items-center gap-1 rounded-full border border-slate-300 bg-white/80 px-2 py-1 text-[11px] font-medium text-slate-700 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800"
onClick={handleToggleTheme}
>
<SunMoon className="w-4 h-4" aria-hidden="true" />
<span> </span>
</button>
</div>
<p className="text-xs text-slate-500 mb-4 dark:text-slate-400"> .</p>
{error && <p className="mb-3 text-xs text-red-300">{error}</p>}
<form onSubmit={handleSubmit} className="space-y-3 text-xs">
<div className="space-y-1">
<label htmlFor="email" className="block text-slate-200">
<label htmlFor="email" className="block text-slate-800 dark:text-slate-200">
</label>
<input
@@ -88,13 +112,13 @@ export default function LoginPage() {
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs text-slate-50 focus:outline-none focus:ring-1 focus:ring-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 focus:outline-none focus:ring-1 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-50"
required
/>
</div>
<div className="space-y-1">
<label htmlFor="password" className="block text-slate-200">
<label htmlFor="password" className="block text-slate-800 dark:text-slate-200">
</label>
<input
@@ -102,7 +126,7 @@ export default function LoginPage() {
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs text-slate-50 focus:outline-none focus:ring-1 focus:ring-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 focus:outline-none focus:ring-1 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-50"
required
minLength={8}
/>
@@ -117,7 +141,7 @@ export default function LoginPage() {
</button>
</form>
<p className="mt-4 text-[11px] text-slate-400">
<p className="mt-4 text-[11px] text-slate-500 dark:text-slate-400">
{" "}
<Link href="/signup" className="text-sky-300 hover:text-sky-200 underline-offset-2 hover:underline">
+2 -2
View File
@@ -12,8 +12,8 @@ export default function HomePage() {
}, [router]);
return (
<main className="flex min-h-screen items-center justify-center">
<p className="text-sm text-slate-400"> ...</p>
<main className="flex min-h-screen items-center justify-center bg-slate-100 text-slate-900 dark:bg-slate-950 dark:text-slate-50">
<p className="text-sm text-slate-500 dark:text-slate-400"> ...</p>
</main>
);
}
+13 -10
View File
@@ -91,10 +91,13 @@ export default function PreviewPage() {
const canvasStyle: CSSProperties = {};
const preset = projectConfig?.canvasPreset ?? "full";
const mainStyle: CSSProperties = {};
if (projectConfig?.bodyBgColorHex && projectConfig.bodyBgColorHex.trim()) {
mainStyle.backgroundColor = projectConfig.bodyBgColorHex;
}
const mainStyle: CSSProperties = {};
{
const raw = typeof projectConfig?.bodyBgColorHex === "string" ? projectConfig.bodyBgColorHex.trim() : "";
if (raw) {
mainStyle.backgroundColor = raw;
}
}
const handleExportZip = async () => {
try {
@@ -158,16 +161,16 @@ export default function PreviewPage() {
}
return (
<main className="min-h-screen flex flex-col bg-slate-950 text-slate-50" style={mainStyle}>
<header className="border-b border-slate-800 px-6 py-4 flex items-center justify-between">
<main className="min-h-screen flex flex-col" style={mainStyle}>
<header className="border-b border-slate-200 px-6 py-4 flex items-center justify-between bg-white/80 backdrop-blur dark:border-slate-800 dark:bg-slate-950/80">
<div>
<h1 className="text-xl font-semibold">Page Preview</h1>
<p className="text-xs text-slate-400"> </p>
<p className="text-xs text-slate-500 dark:text-slate-400"> </p>
</div>
<div className="flex items-center gap-2 text-xs">
<button
type="button"
className="inline-flex items-center rounded border border-slate-700 bg-slate-900 px-3 py-1 text-slate-100 hover:bg-slate-800"
className="inline-flex items-center rounded border border-slate-300 bg-white px-3 py-1 text-slate-800 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={() => {
void handleExportZip();
}}
@@ -176,13 +179,13 @@ export default function PreviewPage() {
</button>
<Link
href="/projects"
className="inline-flex items-center rounded border border-slate-700 bg-slate-900 px-3 py-1 text-xs text-slate-100 hover:bg-slate-800"
className="inline-flex items-center rounded border border-slate-300 bg-white px-3 py-1 text-xs text-slate-800 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
>
</Link>
<Link
href={editorHref}
className="inline-flex items-center rounded border border-slate-700 bg-slate-900 px-3 py-1 text-xs text-slate-100 hover:bg-slate-800"
className="inline-flex items-center rounded border border-slate-300 bg-white px-3 py-1 text-xs text-slate-800 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
>
</Link>
+109 -28
View File
@@ -1,7 +1,9 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter, useParams } from "next/navigation";
import { FolderKanban, LayoutDashboard, ListChecks, SunMoon } from "lucide-react";
// 프로젝트별 폼 제출 내역을 조회해서 보여주는 페이지 컴포넌트.
// - URL 의 slug 파라미터를 기준으로 /api/projects/[slug]/submissions 에 요청을 보낸다.
@@ -26,6 +28,7 @@ export default function ProjectSubmissionsPage() {
const [submissions, setSubmissions] = useState<SubmissionItem[]>([]);
const [status, setStatus] = useState<PageStatus>("idle");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
// 마운트 시 현재 slug 기준으로 폼 제출 내역을 불러온다.
useEffect(() => {
@@ -102,46 +105,121 @@ export default function ProjectSubmissionsPage() {
.join("\n");
};
const handleLogout = async () => {
try {
const res = await fetch("/api/auth/logout", {
method: "POST",
});
if (!res.ok) {
setErrorMessage("로그아웃 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
return;
}
router.push("/login");
} catch {
setErrorMessage("로그아웃 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
}
};
const handleToggleTheme = () => {
if (typeof document === "undefined") {
return;
}
const root = document.documentElement;
if (!root) {
return;
}
root.classList.toggle("dark");
};
return (
<main className="min-h-screen flex flex-col bg-slate-950 text-slate-50">
<header className="border-b border-slate-800 px-6 py-4 flex items-center justify-between bg-slate-950/80 backdrop-blur">
<main className="min-h-screen flex flex-col bg-slate-100 text-slate-900 dark:bg-slate-950 dark:text-slate-50">
<header className="border-b border-slate-200 px-6 py-4 flex items-center justify-between bg-white/80 backdrop-blur dark:border-slate-800 dark:bg-slate-950/80">
<div>
<h1 className="text-xl font-semibold"> </h1>
<p className="text-xs text-slate-400"> .</p>
<h1 className="text-2xl font-bold tracking-tight"> </h1>
<p className="text-xs text-slate-500 dark:text-slate-400"> .</p>
<p className="mt-1 text-[11px] font-mono text-slate-500 dark:text-slate-400">{slug}</p>
</div>
<div className="text-xs text-slate-300 flex flex-col items-end gap-1">
<span className="font-mono text-[11px]">{slug}</span>
<a
href="/projects"
role="link"
className="text-[11px] text-sky-400 hover:text-sky-300 underline-offset-2 hover:underline"
onClick={(event) => {
event.preventDefault();
router.push("/projects");
}}
<div className="flex items-center gap-3 text-sm">
<nav className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white/80 px-1.5 py-1 shadow-sm dark:border-slate-700 dark:bg-slate-900/80">
<Link
href="/dashboard"
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium text-slate-700 hover:bg-slate-100 hover:text-sky-700 dark:text-slate-200 dark:hover:bg-slate-800 dark:hover:text-sky-200"
>
<LayoutDashboard className="w-4 h-4" aria-hidden="true" />
<span></span>
</Link>
<Link
href="/projects"
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium text-slate-700 hover:bg-slate-100 hover:text-sky-700 dark:text-slate-200 dark:hover:bg-slate-800 dark:hover:text-sky-200"
>
<FolderKanban className="w-4 h-4" aria-hidden="true" />
<span> </span>
</Link>
<Link
href="/projects/submissions"
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium text-emerald-700 hover:bg-emerald-50 hover:text-emerald-800 dark:text-emerald-200 dark:hover:bg-emerald-900/60 dark:hover:text-emerald-100"
>
<ListChecks className="w-4 h-4" aria-hidden="true" />
<span> </span>
</Link>
</nav>
<button
type="button"
className="inline-flex items-center gap-1 rounded-full border border-slate-300 bg-white/80 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800"
onClick={handleToggleTheme}
>
</a>
<SunMoon className="w-4 h-4" aria-hidden="true" />
<span> </span>
</button>
<div className="relative">
<button
type="button"
className="inline-flex items-center gap-1 rounded-full border border-slate-300 bg-white/80 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800"
onClick={() => {
setIsMenuOpen((prev) => !prev);
}}
>
</button>
{isMenuOpen && (
<div className="absolute right-0 mt-1 w-32 rounded border border-slate-200 bg-white shadow-lg z-20 dark:border-slate-700 dark:bg-slate-900/95">
<button
type="button"
className="w-full px-3 py-2 text-left text-xs text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800"
onClick={() => {
setIsMenuOpen(false);
void handleLogout();
}}
>
</button>
</div>
)}
</div>
</div>
</header>
<section className="flex-1 px-6 py-4 overflow-auto">
<section className="flex-1 px-6 py-4 overflow-auto bg-slate-50/60 dark:bg-transparent">
{status === "error" && errorMessage && (
<p className="text-xs text-red-300 mb-3">{errorMessage}</p>
<p className="text-xs text-red-500 mb-3 dark:text-red-300">{errorMessage}</p>
)}
{status === "loading" && (
<p className="text-xs text-slate-400 mb-3"> ...</p>
<p className="text-xs text-slate-500 mb-3 dark:text-slate-400"> ...</p>
)}
{submissions.length === 0 && status === "idle" && !errorMessage && (
<p className="text-xs text-slate-400"> .</p>
<p className="text-xs text-slate-500 dark:text-slate-400"> .</p>
)}
{submissions.length > 0 && (
<table className="w-full text-xs text-left border-collapse mt-2">
<thead>
<tr className="border-b border-slate-800 text-slate-400">
<tr className="border-b border-slate-200 text-slate-600 dark:border-slate-800 dark:text-slate-400">
<th className="py-2 pr-4"> </th>
<th className="py-2 pr-4"></th>
<th className="py-2 pr-4"></th>
@@ -165,15 +243,18 @@ export default function ProjectSubmissionsPage() {
const birthdate = typeof birthdateValue === "string" ? birthdateValue : "";
return (
<tr key={item.id} className="border-b border-slate-900 hover:bg-slate-900/60">
<td className="py-2 pr-4 text-slate-300 text-[11px]">
<tr
key={item.id}
className="border-b border-slate-200 hover:bg-slate-50 dark:border-slate-900 dark:hover:bg-slate-900/60"
>
<td className="py-2 pr-4 text-slate-600 dark:text-slate-300 text-[11px]">
{new Date(item.createdAt).toLocaleString()}
</td>
<td className="py-2 pr-4 text-slate-100">{name}</td>
<td className="py-2 pr-4 text-slate-300">{email}</td>
<td className="py-2 pr-4 text-slate-300">{phone}</td>
<td className="py-2 pr-4 text-slate-300">{birthdate}</td>
<td className="py-2 pr-4 text-slate-300 whitespace-pre-wrap">
<td className="py-2 pr-4 text-slate-900 dark:text-slate-100">{name}</td>
<td className="py-2 pr-4 text-slate-600 dark:text-slate-300">{email}</td>
<td className="py-2 pr-4 text-slate-600 dark:text-slate-300">{phone}</td>
<td className="py-2 pr-4 text-slate-600 dark:text-slate-300">{birthdate}</td>
<td className="py-2 pr-4 text-slate-600 dark:text-slate-300 whitespace-pre-wrap">
{renderOtherFields(payload)}
</td>
</tr>
+23 -20
View File
@@ -292,11 +292,11 @@ export default function ProjectsPage() {
<>
<div
data-testid="projects-toolbar"
className="flex items-center justify-between mb-2 text-[11px] text-slate-300"
className="flex items-center justify-between mb-2 text-[11px] text-slate-600 dark:text-slate-300"
>
<div className="inline-flex items-center gap-2">
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-slate-900/70 border border-slate-700">
<ListChecks className="w-3 h-3 text-slate-400" aria-hidden="true" />
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full border bg-slate-100 border-slate-300 text-slate-700 dark:bg-slate-900/70 dark:border-slate-700 dark:text-slate-200">
<ListChecks className="w-3 h-3 text-slate-400 dark:text-slate-500" aria-hidden="true" />
<span>
: <span className="font-semibold">{selectedSlugs.length}</span>
</span>
@@ -305,7 +305,7 @@ export default function ProjectsPage() {
<div className="flex items-center gap-2">
<button
type="button"
className="inline-flex items-center gap-1 rounded border border-red-700 px-2 py-1 text-red-200 hover:bg-red-900/40 disabled:opacity-40 disabled:cursor-not-allowed"
className="inline-flex items-center gap-1 rounded border px-2 py-1 text-xs font-medium border-red-300 text-red-700 bg-red-50 hover:bg-red-100 disabled:opacity-40 disabled:cursor-not-allowed dark:border-red-700 dark:text-red-200 dark:bg-red-900/40 dark:hover:bg-red-900/60"
disabled={selectedSlugs.length === 0}
onClick={() => {
void handleBulkDelete();
@@ -316,7 +316,7 @@ export default function ProjectsPage() {
</button>
<Link
href="/editor?new=1"
className="inline-flex items-center gap-1 rounded border border-sky-700 bg-sky-950 px-2 py-1 text-sky-100 hover:bg-sky-900 shadow-sm"
className="inline-flex items-center gap-1 rounded border px-2 py-1 text-xs font-medium border-sky-500 bg-sky-600 text-white hover:bg-sky-700 shadow-sm dark:border-sky-700 dark:bg-sky-950 dark:text-sky-100 dark:hover:bg-sky-900"
>
<FilePlus2 className="w-3 h-3" aria-hidden="true" />
<span> </span>
@@ -325,7 +325,7 @@ export default function ProjectsPage() {
</div>
<table className="w-full text-xs text-left border-collapse mt-2">
<thead>
<tr className="border-b border-slate-800 text-slate-400">
<tr className="border-b border-slate-200 text-slate-600 dark:border-slate-800 dark:text-slate-400">
<th className="py-2 pr-2 w-8">
<input
type="checkbox"
@@ -357,7 +357,10 @@ export default function ProjectsPage() {
{pageProjects.map((project) => {
const checked = selectedSlugs.includes(project.slug);
return (
<tr key={project.id} className="border-b border-slate-900 hover:bg-slate-900/60">
<tr
key={project.id}
className="border-b border-slate-200 hover:bg-slate-50 dark:border-slate-900 dark:hover:bg-slate-900/60"
>
<td className="py-2 pr-2 w-8">
<input
type="checkbox"
@@ -375,10 +378,10 @@ export default function ProjectsPage() {
}}
/>
</td>
<td className="py-2 pr-4 text-slate-100">{project.title}</td>
<td className="py-2 pr-4 text-slate-300 font-mono text-[11px]">{project.slug}</td>
<td className="py-2 pr-4 text-slate-300">{project.status}</td>
<td className="py-2 pr-4 text-slate-500 text-[11px]">
<td className="py-2 pr-4 text-slate-900 dark:text-slate-100">{project.title}</td>
<td className="py-2 pr-4 text-slate-600 dark:text-slate-300 font-mono text-[11px]">{project.slug}</td>
<td className="py-2 pr-4 text-slate-600 dark:text-slate-300">{project.status}</td>
<td className="py-2 pr-4 text-slate-500 dark:text-slate-400 text-[11px]">
{new Date(project.createdAt).toLocaleString()}
</td>
<td className="py-2 pr-4 text-slate-500 text-[11px]">
@@ -388,28 +391,28 @@ export default function ProjectsPage() {
<div className="inline-flex items-center gap-2">
<Link
href={`/editor?slug=${encodeURIComponent(project.slug)}`}
className="inline-flex items-center gap-1 text-sky-300 hover:text-sky-200 underline-offset-2 hover:underline"
className="inline-flex items-center gap-1 text-sky-600 hover:text-sky-700 underline-offset-2 hover:underline dark:text-sky-300 dark:hover:text-sky-200"
>
<Pencil className="w-3 h-3" aria-hidden="true" />
<span></span>
</Link>
<Link
href={`/preview?slug=${encodeURIComponent(project.slug)}`}
className="inline-flex items-center gap-1 text-slate-300 hover:text-slate-100 underline-offset-2 hover:underline"
className="inline-flex items-center gap-1 text-slate-600 hover:text-slate-800 underline-offset-2 hover:underline dark:text-slate-300 dark:hover:text-slate-100"
>
<Eye className="w-3 h-3" aria-hidden="true" />
<span></span>
</Link>
<Link
href={`/projects/${encodeURIComponent(project.slug)}/submissions`}
className="inline-flex items-center gap-1 text-emerald-300 hover:text-emerald-100 underline-offset-2 hover:underline"
className="inline-flex items-center gap-1 text-emerald-600 hover:text-emerald-700 underline-offset-2 hover:underline dark:text-emerald-300 dark:hover:text-emerald-100"
>
<ListChecks className="w-3 h-3" aria-hidden="true" />
<span> </span>
</Link>
<button
type="button"
className="inline-flex items-center gap-1 text-red-300 hover:text-red-200 underline-offset-2 hover:underline"
className="inline-flex items-center gap-1 text-red-600 hover:text-red-700 underline-offset-2 hover:underline dark:text-red-300 dark:hover:text-red-200"
onClick={() => {
void handleDelete(project.slug);
}}
@@ -425,9 +428,9 @@ export default function ProjectsPage() {
</tbody>
</table>
<div className="mt-3 flex items-center justify-between text-[11px] text-slate-300">
<div className="mt-3 flex items-center justify-between text-[11px] text-slate-600 dark:text-slate-300">
<div className="inline-flex items-center gap-2">
<span className="px-2 py-1 rounded-full bg-slate-900/70 border border-slate-700">
<span className="px-2 py-1 rounded-full bg-slate-100 border border-slate-300 text-slate-700 dark:bg-slate-900/70 dark:border-slate-700 dark:text-slate-200">
{totalCount} {" "}
<span className="font-semibold">
{totalCount === 0 ? 0 : startIndex + 1}{Math.min(endIndex, totalCount)}
@@ -446,7 +449,7 @@ export default function ProjectsPage() {
className={`w-6 h-6 rounded-full text-[10px] flex items-center justify-center border transition-colors ${
isActive
? "bg-sky-600 text-white border-sky-500 shadow-sm"
: "bg-slate-900 text-slate-300 border-slate-700 hover:bg-slate-800"
: "bg-white text-slate-700 border-slate-300 hover:bg-slate-100 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-700 dark:hover:bg-slate-800"
}`}
>
{page}
@@ -458,7 +461,7 @@ export default function ProjectsPage() {
<button
type="button"
disabled={currentPage === 1}
className="inline-flex items-center gap-1 px-2 py-1 rounded border border-slate-700 text-slate-300 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-800"
className="inline-flex items-center gap-1 px-2 py-1 rounded border border-slate-300 text-slate-700 bg-white disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-100 dark:border-slate-700 dark:text-slate-300 dark:bg-slate-900 dark:hover:bg-slate-800"
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
>
<ChevronLeft className="w-3 h-3" aria-hidden="true" />
@@ -467,7 +470,7 @@ export default function ProjectsPage() {
<button
type="button"
disabled={currentPage === totalPages}
className="inline-flex items-center gap-1 px-2 py-1 rounded border border-slate-700 text-slate-300 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-800"
className="inline-flex items-center gap-1 px-2 py-1 rounded border border-slate-300 text-slate-700 bg-white disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-100 dark:border-slate-700 dark:text-slate-300 dark:bg-slate-900 dark:hover:bg-slate-800"
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
>
<span></span>
+13 -10
View File
@@ -197,7 +197,7 @@ export default function AllProjectSubmissionsPage() {
{submissions.length > 0 && (
<table className="w-full text-xs text-left border-collapse mt-2">
<thead>
<tr className="border-b border-slate-800 text-slate-400">
<tr className="border-b border-slate-200 text-slate-600 dark:border-slate-800 dark:text-slate-400">
<th className="py-2 pr-4"> </th>
<th className="py-2 pr-4"></th>
<th className="py-2 pr-4">Slug</th>
@@ -223,17 +223,20 @@ export default function AllProjectSubmissionsPage() {
const birthdate = typeof birthdateValue === "string" ? birthdateValue : "";
return (
<tr key={item.id} className="border-b border-slate-900 hover:bg-slate-900/60">
<td className="py-2 pr-4 text-slate-300 text-[11px]">
<tr
key={item.id}
className="border-b border-slate-200 hover:bg-slate-50 dark:border-slate-900 dark:hover:bg-slate-900/60"
>
<td className="py-2 pr-4 text-slate-600 dark:text-slate-300 text-[11px]">
{new Date(item.createdAt).toLocaleString()}
</td>
<td className="py-2 pr-4 text-slate-100">{item.projectTitle ?? "-"}</td>
<td className="py-2 pr-4 text-slate-300">{item.projectSlug}</td>
<td className="py-2 pr-4 text-slate-100">{name}</td>
<td className="py-2 pr-4 text-slate-300">{email}</td>
<td className="py-2 pr-4 text-slate-300">{phone}</td>
<td className="py-2 pr-4 text-slate-300">{birthdate}</td>
<td className="py-2 pr-4 text-slate-300 whitespace-pre-wrap">
<td className="py-2 pr-4 text-slate-900 dark:text-slate-100">{item.projectTitle ?? "-"}</td>
<td className="py-2 pr-4 text-slate-600 dark:text-slate-300">{item.projectSlug}</td>
<td className="py-2 pr-4 text-slate-900 dark:text-slate-100">{name}</td>
<td className="py-2 pr-4 text-slate-600 dark:text-slate-300">{email}</td>
<td className="py-2 pr-4 text-slate-600 dark:text-slate-300">{phone}</td>
<td className="py-2 pr-4 text-slate-600 dark:text-slate-300">{birthdate}</td>
<td className="py-2 pr-4 text-slate-600 dark:text-slate-300 whitespace-pre-wrap">
{renderOtherFields(payload)}
</td>
</tr>
+85 -11
View File
@@ -4,15 +4,37 @@ import { FormEvent, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
// 회원가입 페이지 컴포넌트
// - 이메일/비밀번호를 입력받아 /api/auth/signup 으로 요청을 전송한다.
// - 성공 시 /projects 로 이동하고, 실패 시 에러 메시지를 보여준다.
type PasswordStrengthLabel = "약함" | "보통" | "강함";
function getPasswordStrengthLabel(password: string): PasswordStrengthLabel | null {
if (!password || password.length < 4) {
return null;
}
let score = 0;
if (/[a-z]/.test(password)) score += 1;
if (/[A-Z]/.test(password)) score += 1;
if (/[0-9]/.test(password)) score += 1;
if (/[^a-zA-Z0-9]/.test(password)) score += 1;
if (password.length >= 12) score += 1;
if (password.length < 8 || score <= 2) {
return "약함";
}
if (score === 3) {
return "보통";
}
return "강함";
}
export default function SignupPage() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [passwordConfirm, setPasswordConfirm] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
@@ -41,6 +63,11 @@ export default function SignupPage() {
e.preventDefault();
setError(null);
if (password !== passwordConfirm) {
setError("비밀번호와 비밀번호 확인이 일치하지 않습니다.");
return;
}
setLoading(true);
try {
@@ -69,17 +96,19 @@ export default function SignupPage() {
}
};
const strengthLabel = getPasswordStrengthLabel(password);
return (
<main className="min-h-screen flex items-center justify-center bg-slate-950 text-slate-50">
<div className="w-full max-w-sm rounded-lg border border-slate-800 bg-slate-900/70 p-6 shadow-xl">
<main className="min-h-screen flex items-center justify-center bg-slate-100 text-slate-900 dark:bg-slate-950 dark:text-slate-50">
<div className="w-full max-w-sm rounded-lg border border-slate-200 bg-white p-6 shadow-xl dark:border-slate-800 dark:bg-slate-900/70">
<h1 className="text-lg font-semibold mb-1"></h1>
<p className="text-xs text-slate-400 mb-4"> .</p>
<p className="text-xs text-slate-500 mb-4 dark:text-slate-400"> .</p>
{error && <p className="mb-3 text-xs text-red-300">{error}</p>}
<form onSubmit={handleSubmit} className="space-y-3 text-xs">
<div className="space-y-1">
<label htmlFor="email" className="block text-slate-200">
<label htmlFor="email" className="block text-slate-800 dark:text-slate-200">
</label>
<input
@@ -87,13 +116,13 @@ export default function SignupPage() {
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs text-slate-50 focus:outline-none focus:ring-1 focus:ring-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 focus:outline-none focus:ring-1 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-50"
required
/>
</div>
<div className="space-y-1">
<label htmlFor="password" className="block text-slate-200">
<label htmlFor="password" className="block text-slate-800 dark:text-slate-200">
</label>
<input
@@ -101,7 +130,52 @@ export default function SignupPage() {
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs text-slate-50 focus:outline-none focus:ring-1 focus:ring-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 focus:outline-none focus:ring-1 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-50"
required
minLength={8}
/>
</div>
{strengthLabel && (
<div className="mt-1 space-y-1">
<div className="h-1.5 rounded-full bg-slate-200/70 dark:bg-slate-700/70 overflow-hidden">
<div
data-testid="password-strength-bar"
className={
"h-full transition-all " +
(strengthLabel === "약함"
? "w-1/3 bg-red-500"
: strengthLabel === "보통"
? "w-2/3 bg-amber-500"
: "w-full bg-emerald-600")
}
/>
</div>
<p
className={
"text-[11px] " +
(strengthLabel === "약함"
? "text-red-500 dark:text-red-300"
: strengthLabel === "보통"
? "text-amber-500 dark:text-amber-300"
: "text-emerald-600 dark:text-emerald-300")
}
>
: {strengthLabel}
</p>
</div>
)}
<div className="space-y-1">
<label htmlFor="passwordConfirm" className="block text-slate-800 dark:text-slate-200">
</label>
<input
id="passwordConfirm"
type="password"
value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.target.value)}
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 focus:outline-none focus:ring-1 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-50"
required
minLength={8}
/>
@@ -116,7 +190,7 @@ export default function SignupPage() {
</button>
</form>
<p className="mt-4 text-[11px] text-slate-400">
<p className="mt-4 text-[11px] text-slate-500 dark:text-slate-400">
{" "}
<Link href="/login" className="text-sky-300 hover:text-sky-200 underline-offset-2 hover:underline">
@@ -32,7 +32,7 @@ export type ColorPickerFieldProps = {
// 텍스트/버튼 등에서 공통으로 사용하는 기본 색상 팔레트
export const TEXT_COLOR_PALETTE: ColorPaletteItem[] = [
// 기본/중립 계열
{ id: "default", label: "기본", color: "#e5e7eb" },
{ id: "default", label: "없음", color: "" },
// 투명
{ id: "transparent", label: "투명", color: "transparent" },
{ id: "muted", label: "연한", color: "#9ca3af" },
@@ -101,24 +101,26 @@ export function ColorPickerField({
if (palette.length > 0) {
if (selectedPaletteId) {
selectedPalette = palette.find((item) => item.id === selectedPaletteId) ?? null;
} else if (value) {
} else {
// value 가 빈 문자열이어도 color 가 "" 인 팔레트 항목(예: "없음")을 선택된 상태로 인식할 수 있도록
// 항상 color 매칭으로 selectedPalette 를 계산한다.
selectedPalette = palette.find((item) => item.color === value) ?? null;
}
}
return (
<div className="flex flex-col gap-1 text-xs text-slate-400">
<div className="flex flex-col gap-1 text-xs text-slate-500 dark:text-slate-400">
<span>{label}</span>
<div className="flex items-center gap-2">
<input
type="color"
aria-label={ariaLabelColorInput}
className="h-8 w-8 rounded border border-slate-700 bg-slate-900 p-0"
className="h-8 w-8 rounded border border-slate-300 bg-white p-0 dark:border-slate-700 dark:bg-slate-900"
value={colorInputValue}
onChange={handleColorInputChange}
/>
<input
className="w-28 flex-none rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-28 flex-none rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label={ariaLabelHexInput}
placeholder="예: #ff0000"
value={value}
@@ -128,30 +130,30 @@ export function ColorPickerField({
<div className="relative text-[11px] flex-none">
<button
type="button"
className="w-28 rounded border border-slate-800 bg-slate-950/60 flex items-center justify-between px-2 py-1 text-left"
className="w-28 rounded border border-slate-300 bg-white flex items-center justify-between px-2 py-1 text-left text-slate-900 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-100"
onClick={() => setOpen((prev) => !prev)}
>
<span className="flex items-center gap-1">
<span
className="inline-block h-3 w-3 rounded-full"
className="inline-block h-3 w-3 rounded-full border border-slate-300 dark:border-slate-700"
style={{ backgroundColor: selectedPalette?.color ?? value ?? "#ffffff" }}
/>
<span className="truncate text-slate-200">
<span className="truncate text-slate-700 dark:text-slate-100">
{selectedPalette?.label ?? "색상 팔레트"}
</span>
</span>
<span className="text-slate-500 text-[10px]"></span>
<span className="text-slate-400 dark:text-slate-500 text-[10px]"></span>
</button>
{open ? (
<div className="absolute right-0 top-full mt-1 w-32 rounded border border-slate-800 bg-slate-950 max-h-40 overflow-auto z-10">
<div className="absolute right-0 top-full mt-1 w-32 rounded border border-slate-300 bg-white max-h-40 overflow-auto z-10 dark:border-slate-800 dark:bg-slate-950">
{palette.map((item) => (
<button
key={item.id}
type="button"
className={`w-full flex items-center justify-between px-2 py-1 text-left text-[11px] border-b border-slate-900 last:border-b-0 ${
className={`w-full flex items-center justify-between px-2 py-1 text-left text-[11px] border-b border-slate-200 last:border-b-0 dark:border-slate-800 ${
selectedPaletteId === item.id
? "bg-sky-900/40 text-sky-100"
: "bg-transparent text-slate-300 hover:bg-slate-900/60"
? "bg-sky-100 text-sky-900 dark:bg-sky-900/40 dark:text-sky-100"
: "bg-white text-slate-900 hover:bg-slate-100 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
}`}
onClick={(event) => {
event.stopPropagation();
@@ -161,7 +163,7 @@ export function ColorPickerField({
>
<span className="flex items-center gap-2">
<span
className="inline-block h-3 w-3 rounded-full"
className="inline-block h-3 w-3 rounded-full border border-slate-300 dark:border-slate-700"
style={{ backgroundColor: item.color }}
/>
<span>{item.label}</span>
@@ -60,7 +60,7 @@ export function NumericPropertyControl({
};
return (
<label className="flex flex-col gap-1 text-xs text-slate-400">
<label className="flex flex-col gap-1 text-xs text-slate-500 dark:text-slate-400">
<span>
{label}
{unitLabel ? ` ${unitLabel}` : ""}
@@ -68,7 +68,7 @@ export function NumericPropertyControl({
<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"
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}
@@ -45,7 +45,7 @@ export function PropertySliderField({
};
return (
<label className="flex flex-col gap-1 text-xs text-slate-400">
<label className="flex flex-col gap-1 text-xs text-slate-500 dark:text-slate-400">
<span>{label}</span>
<div className="flex items-center gap-2">
<input
@@ -60,7 +60,7 @@ export function PropertySliderField({
/>
</div>
<input
className="w-full rounded border border-slate-700 bg-slate-900 px-2 py-1 text-xs outline-none focus:border-sky-500"
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label={ariaLabelInput}
value={Number.isFinite(value) ? value : ""}
onChange={handleInputChange}
@@ -101,9 +101,6 @@ const convertPxStringToEm = (value?: string | null) => {
};
export function PublicPageRenderer({ blocks, projectSlug }: PublicPageRendererProps) {
const sectionBlocks = blocks.filter((b) => b.type === "section");
const rootBlocks = blocks.filter((b) => !b.sectionId && b.type !== "section");
const [formStatus, setFormStatus] = useState<"idle" | "submitting" | "success" | "error">("idle");
const [formMessage, setFormMessage] = useState<string>("");
@@ -329,7 +326,7 @@ export function PublicPageRenderer({ blocks, projectSlug }: PublicPageRendererPr
const isRequired = requiredFieldIdSet.has(block.id);
const labelDisplay = (props as any).labelDisplay ?? "visible";
const wrapperClassName = `${tokens.wrapperLayoutClass} text-xs text-slate-200`;
const wrapperClassName = `${tokens.wrapperLayoutClass} text-xs`;
const rawPlaceholder =
typeof props.placeholder === "string" ? props.placeholder.trim() : "";
@@ -355,7 +352,9 @@ export function PublicPageRenderer({ blocks, projectSlug }: PublicPageRendererPr
? (props as any).paddingY
: null;
const floatingFieldStyle: CSSProperties = {};
// 플로팅 필드 wrapper 에도 토큰 기반 wrapperStyle 을 전달해,
// --pb-input-border-color 와 같은 CSS 변수가 pb-form-field--floating 에 적용되도록 한다.
const floatingFieldStyle: CSSProperties = { ...tokens.wrapperStyle };
if (paddingY !== null) {
(floatingFieldStyle as any)["--pb-input-padding-y"] = `${paddingY / 16}rem`;
}
@@ -391,7 +390,7 @@ export function PublicPageRenderer({ blocks, projectSlug }: PublicPageRendererPr
/>
)}
{helperText && (
<p className="mt-1 text-[10px] text-slate-400">{helperText}</p>
<p className="mt-1 text-[10px]">{helperText}</p>
)}
<label htmlFor={inputId} className="pb-form-label">
{props.label}
@@ -523,7 +522,7 @@ export function PublicPageRenderer({ blocks, projectSlug }: PublicPageRendererPr
const gapPx = typeof (props as any).labelGapPx === "number" ? (props as any).labelGapPx : 8;
const gapEm = pxToEm(gapPx);
const baseClass = ["text-slate-200", tokens.widthClass].filter(Boolean).join(" ");
const baseClass = [tokens.widthClass].filter(Boolean).join(" ");
const layoutClass = isInlineLayout ? "flex flex-row items-center" : "flex flex-col gap-1";
const groupStyle: CSSProperties = {
@@ -563,7 +562,7 @@ export function PublicPageRenderer({ blocks, projectSlug }: PublicPageRendererPr
>
<input
type="checkbox"
className="h-4 w-4 rounded border-slate-700 bg-slate-900 text-sky-500"
className="h-4 w-4 rounded"
name={props.formFieldName}
value={opt.value}
required={isRequired && index === 0}
@@ -616,7 +615,7 @@ export function PublicPageRenderer({ blocks, projectSlug }: PublicPageRendererPr
const gapPx = typeof (props as any).labelGapPx === "number" ? (props as any).labelGapPx : 8;
const gapEm = pxToEm(gapPx);
const baseClass = ["text-slate-200", tokens.widthClass].filter(Boolean).join(" ");
const baseClass = [tokens.widthClass].filter(Boolean).join(" ");
const layoutClass = isInlineLayout ? "flex flex-row items-center" : "flex flex-col gap-1";
const groupStyle: CSSProperties = {
@@ -655,7 +654,7 @@ export function PublicPageRenderer({ blocks, projectSlug }: PublicPageRendererPr
>
<input
type="radio"
className="h-4 w-4 border-slate-700 bg-slate-900 text-sky-500"
className="h-4 w-4"
name={props.formFieldName}
value={opt.value}
required={isRequired && index === 0}
@@ -1054,21 +1053,49 @@ export function PublicPageRenderer({ blocks, projectSlug }: PublicPageRendererPr
);
};
return (
<div className="flex-1 flex flex-col text-slate-50">
{/* 루트 텍스트/버튼/이미지 블록들이 있다면 pb-root 내에 페이지 상단에 노출 */}
{rootBlocks.length > 0 && (
<section className="pb-root">
<div className="pb-root-inner">
{rootBlocks.map((b) => (
<div key={b.id}>{renderBlock(b)}</div>
))}
</div>
</section>
)}
type LayoutChunk =
| { kind: "root"; blocks: Block[] }
| { kind: "section"; section: Block };
{/* 섹션 블록들 */}
{sectionBlocks.map((section) => renderSection(section))}
const layoutChunks: LayoutChunk[] = [];
for (const block of blocks) {
if (block.type === "section") {
layoutChunks.push({ kind: "section", section: block });
continue;
}
if (!block.sectionId) {
const last = layoutChunks[layoutChunks.length - 1];
if (!last || last.kind !== "root") {
layoutChunks.push({ kind: "root", blocks: [block] });
} else {
last.blocks.push(block);
}
}
}
return (
<div className="flex-1 flex flex-col">
{layoutChunks.map((chunk, index) => {
if (chunk.kind === "root") {
if (chunk.blocks.length === 0) {
return null;
}
return (
<section key={`root-${index}`} className="pb-root">
<div className="pb-root-inner">
{chunk.blocks.map((b) => (
<div key={b.id}>{renderBlock(b)}</div>
))}
</div>
</section>
);
}
return renderSection(chunk.section);
})}
</div>
);
}
+4 -4
View File
@@ -623,7 +623,10 @@ export const computeFormInputPublicTokens = (props: FormInputBlockProps): FormIn
}
if (props.strokeColorCustom && props.strokeColorCustom.trim() !== "") {
inputStyle.borderColor = props.strokeColorCustom.trim();
const borderColor = props.strokeColorCustom.trim();
inputStyle.borderColor = borderColor;
// 플로팅 라벨 패치가 필드 테두리 색을 참조할 수 있도록 CSS 변수에 전달한다.
(wrapperStyle as any)["--pb-input-border-color"] = borderColor;
}
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
@@ -814,9 +817,6 @@ const computeOptionGroupPublicTokensBase = (
const colorValue = props.textColorCustom.trim();
groupTextStyle.color = colorValue;
optionTextStyle.color = colorValue;
} else {
groupTextStyle.color = "#e5e7eb";
optionTextStyle.color = "#e5e7eb";
}
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
+19 -13
View File
@@ -28,6 +28,9 @@
--pb-color-text-accent: #38bdf8; /* sky-400 */
--pb-color-text-danger: #f97373; /* red-400 근처 */
/* Form input border 기본 색상 (플로팅 라벨 패치 등에서 재사용) */
--pb-color-input-border: #4b5563; /* slate-600 근처 */
/* Text max width */
--pb-text-maxw-prose: 60ch;
--pb-text-maxw-narrow: 40ch;
@@ -150,7 +153,7 @@ body {
/* Text color palette */
.pb-text-color-default {
color: var(--pb-color-text-default);
color: inherit;
}
.pb-text-color-muted {
color: var(--pb-color-text-muted);
@@ -433,6 +436,8 @@ body {
display: flex;
flex-direction: column;
gap: 0.25rem;
/* 플로팅 라벨 패치가 참조하는 입력 테두리 색 기본값 */
--pb-input-border-color: var(--pb-color-input-border);
}
.pb-form-field--floating {
@@ -449,10 +454,9 @@ body {
top: calc(var(--pb-input-padding-y) + 0.3rem);
left: 0.75rem;
font-size: 0.75rem;
color: #9ca3af;
pointer-events: none;
transform-origin: left top;
transition: top 0.15s ease, font-size 0.15s ease, color 0.15s ease;
transition: top 0.3s ease, font-size 0.3s ease, color 0.3s ease, background 0.3s ease;
}
.pb-form-field--floating .pb-input,
@@ -470,16 +474,22 @@ body {
.pb-form-field--floating .pb-input:not(:placeholder-shown) ~ .pb-form-label,
.pb-form-field--floating .pb-textarea:focus ~ .pb-form-label,
.pb-form-field--floating .pb-textarea:not(:placeholder-shown) ~ .pb-form-label {
top: -0.3rem; /* 인풋 상단 보더에 살짝 겹치는 정도로만 띄운다. */
top: -0.425rem; /* 인풋 상단 보더에 살짝 겹치는 정도로만 띄운다. */
font-size: 0.725rem;
color: #e5e7eb;
background-color: #020617; /* 인풋 배경과 맞춰서 보더 라인을 자연스럽게 가린다. */
padding: 0 0.25rem;
padding: 0.25rem 1.725rem;
color: #bbf7d0;
/* 필드 테두리 (또는 기본값) 사용해 라벨 뒤에 부드러운 패치를 그린다.
가운데는 var(--pb-input-border-color), 끝은 투명으로 페이드되도록 설정한다. */
background: radial-gradient(
ellipse closest-side,
var(--pb-input-border-color) 70%,
transparent
);
background-repeat: no-repeat;
}
.pb-form-label {
font-size: 0.75rem;
color: #e5e7eb;
}
.pb-form-options {
@@ -501,20 +511,17 @@ body {
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
color: #e5e7eb;
}
.pb-input,
.pb-select,
.pb-textarea {
width: 100%;
border: 1px solid var(--pb-color-input-border);
border-radius: 0.375rem;
border: 1px solid #1f2937;
background-color: #020617;
/* 세로 패딩은 CSS 변수로 정의해 플로팅 라벨/에디터 등이 함께 참조할 수 있게 한다. */
padding: var(--pb-input-padding-y, 0.5rem) 0.75rem;
font-size: 0.75rem;
color: #e5e7eb;
box-sizing: border-box;
}
@@ -534,7 +541,6 @@ body {
padding-left: 1.25rem;
font-size: 0.875rem;
line-height: var(--pb-leading-normal);
color: var(--pb-color-text-default);
}
/* 리스트 아이템 여백은 --pb-list-gap 커스텀 프로퍼티로 제어한다.
+55
View File
@@ -8,3 +8,58 @@
/* 초기 상태에서는 별도 규칙 없이 파일만 생성해 두고,
* 이후 에디터 전용 레이아웃/패널 스타일을 점진적으로 이동시킨다.
*/
/*
* 에디터/프리뷰용 스크롤바 테마
* - .pb-scroll 클래스를 사용하는 컨테이너에서 라이트/다크 테마에 맞는 스크롤바 색상을 적용한다.
* - builder.css .pb-scroll 기본값은 다크 톤이므로, 여기서는 Tailwind dark 모드(html.dark) 기준으로
* 라이트/다크 테마별로 명시적으로 override 한다.
*/
/* 라이트 모드: 밝은 배경에 어울리는 연한 스크롤바 */
html:not(.dark) .pb-scroll {
scrollbar-width: thin;
scrollbar-color: #94a3b8 #e5e7eb; /* thumb / track */
}
html:not(.dark) .pb-scroll::-webkit-scrollbar {
width: 8px;
}
html:not(.dark) .pb-scroll::-webkit-scrollbar-track {
background-color: #e5e7eb; /* slate-200 */
}
html:not(.dark) .pb-scroll::-webkit-scrollbar-thumb {
background-color: #94a3b8; /* slate-400 */
border-radius: 9999px;
border: 2px solid #e5e7eb;
}
html:not(.dark) .pb-scroll::-webkit-scrollbar-thumb:hover {
background-color: #64748b; /* slate-500 */
}
/* 다크 모드: 기존 builder.css 톤과 유사한 다크 스크롤바 */
html.dark .pb-scroll {
scrollbar-width: thin;
scrollbar-color: #1f2937 #020617; /* thumb / track */
}
html.dark .pb-scroll::-webkit-scrollbar {
width: 8px;
}
html.dark .pb-scroll::-webkit-scrollbar-track {
background-color: #020617; /* slate-950 */
}
html.dark .pb-scroll::-webkit-scrollbar-thumb {
background-color: #1f2937; /* slate-800 */
border-radius: 9999px;
border: 2px solid #020617;
}
html.dark .pb-scroll::-webkit-scrollbar-thumb:hover {
background-color: #374151; /* slate-700 */
}
+1
View File
@@ -1,3 +1,4 @@
@config "../../tailwind.config.js";
@import "tailwindcss";
html,