TDD,E2E 개선, 미적용 스타일 개선
This commit is contained in:
@@ -42,6 +42,12 @@ const normalizeTextColor = (raw: unknown): string => {
|
||||
return trimmed !== "" ? trimmed : "";
|
||||
};
|
||||
|
||||
// Export/Public 레이어용 모서리 둥글기 매핑
|
||||
// - none: 0px
|
||||
// - sm: 2px
|
||||
// - lg: 6px
|
||||
// - full: 9999px (완전 둥근 모서리)
|
||||
// - md(기본): 4px
|
||||
const computeRadiusPx = (radius: unknown): number => {
|
||||
const token = typeof radius === "string" ? radius : "md";
|
||||
if (token === "none") return 0;
|
||||
@@ -51,6 +57,19 @@ const computeRadiusPx = (radius: unknown): number => {
|
||||
return 4;
|
||||
};
|
||||
|
||||
// Editor 레이어용 모서리 둥글기 매핑
|
||||
// - none: 0px
|
||||
// - sm: 4px
|
||||
// - md: 8px (기본)
|
||||
// - lg/full: 9999px (pill 스타일)
|
||||
const computeEditorRadiusPx = (radius: unknown): number => {
|
||||
const token = typeof radius === "string" ? radius : "md";
|
||||
if (token === "none") return 0;
|
||||
if (token === "sm") return 4;
|
||||
if (token === "lg" || token === "full") return 9999;
|
||||
return 8;
|
||||
};
|
||||
|
||||
const computeWidthMode = (props: { widthMode?: string; fullWidth?: boolean }): "auto" | "full" | "fixed" => {
|
||||
if (typeof props.widthMode === "string") {
|
||||
return props.widthMode as "auto" | "full" | "fixed";
|
||||
@@ -101,9 +120,68 @@ export const computeFormInputExportTokens = (
|
||||
inputStyleParts.push(`width:${em}em`);
|
||||
}
|
||||
|
||||
const radiusPx = computeRadiusPx(props.borderRadius);
|
||||
const radiusToken = typeof props.borderRadius === "string" ? props.borderRadius : "md";
|
||||
let radiusPx = 4;
|
||||
if (radiusToken === "none") {
|
||||
radiusPx = 0;
|
||||
} else if (radiusToken === "sm") {
|
||||
radiusPx = 2;
|
||||
} else if (radiusToken === "lg") {
|
||||
radiusPx = 6;
|
||||
} else if (radiusToken === "full") {
|
||||
radiusPx = 9999;
|
||||
}
|
||||
inputStyleParts.push(`border-radius:${radiusPx}px`);
|
||||
|
||||
// 폰트 관련 커스텀 값: px 단위일 경우 em 으로 변환하고, 그렇지 않으면 원문 값을 그대로 사용한다.
|
||||
if (typeof props.fontSizeCustom === "string" && props.fontSizeCustom.trim() !== "") {
|
||||
const raw = props.fontSizeCustom.trim();
|
||||
if (raw.endsWith("px")) {
|
||||
const match = raw.match(/-?\d+(?:\.\d+)?/);
|
||||
if (match) {
|
||||
const px = parseFloat(match[0]);
|
||||
if (Number.isFinite(px)) {
|
||||
const em = px / 16;
|
||||
inputStyleParts.push(`font-size:${em}em`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
inputStyleParts.push(`font-size:${escapeAttr(raw)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof props.lineHeightCustom === "string" && props.lineHeightCustom.trim() !== "") {
|
||||
const raw = props.lineHeightCustom.trim();
|
||||
if (raw.endsWith("px")) {
|
||||
const match = raw.match(/-?\d+(?:\.\d+)?/);
|
||||
if (match) {
|
||||
const px = parseFloat(match[0]);
|
||||
if (Number.isFinite(px)) {
|
||||
const em = px / 16;
|
||||
inputStyleParts.push(`line-height:${em}em`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
inputStyleParts.push(`line-height:${escapeAttr(raw)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof props.letterSpacingCustom === "string" && props.letterSpacingCustom.trim() !== "") {
|
||||
const raw = props.letterSpacingCustom.trim();
|
||||
if (raw.endsWith("px")) {
|
||||
const match = raw.match(/-?\d+(?:\.\d+)?/);
|
||||
if (match) {
|
||||
const px = parseFloat(match[0]);
|
||||
if (Number.isFinite(px)) {
|
||||
const em = px / 16;
|
||||
inputStyleParts.push(`letter-spacing:${em}em`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
inputStyleParts.push(`letter-spacing:${escapeAttr(raw)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const inputStyleAttr = inputStyleParts.length > 0 ? ` style="${inputStyleParts.join(";")}"` : "";
|
||||
|
||||
return {
|
||||
@@ -254,14 +332,9 @@ export const computeFormInputEditorTokens = (props: FormInputBlockProps): FormIn
|
||||
fieldStyle.width = `${props.widthPx}px`;
|
||||
}
|
||||
|
||||
const radius = props.borderRadius ?? "md";
|
||||
if (radius === "none") {
|
||||
fieldStyle.borderRadius = 0;
|
||||
} else if (radius === "sm") {
|
||||
fieldStyle.borderRadius = 4;
|
||||
} else if (radius === "lg" || radius === "full") {
|
||||
fieldStyle.borderRadius = 9999;
|
||||
}
|
||||
// 모서리 둥글기: Editor 레이어에서는 lg/full 토큰을 pill 형태(9999px)로 사용한다.
|
||||
const radiusPx = computeEditorRadiusPx(props.borderRadius);
|
||||
fieldStyle.borderRadius = radiusPx;
|
||||
|
||||
// 에디터에서는 px 기반 padding/타이포 값을 그대로 fieldStyle 에 반영해 미리보기에서 변화가 보이도록 한다.
|
||||
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
|
||||
@@ -336,14 +409,9 @@ export const computeFormSelectEditorTokens = (props: FormSelectBlockProps): Form
|
||||
fieldStyle.width = `${props.widthPx}px`;
|
||||
}
|
||||
|
||||
const radius = props.borderRadius ?? "md";
|
||||
if (radius === "none") {
|
||||
fieldStyle.borderRadius = 0;
|
||||
} else if (radius === "sm") {
|
||||
fieldStyle.borderRadius = 4;
|
||||
} else if (radius === "lg" || radius === "full") {
|
||||
fieldStyle.borderRadius = 9999;
|
||||
}
|
||||
// 모서리 둥글기: Editor 레이어에서는 lg/full 토큰을 pill 형태(9999px)로 사용한다.
|
||||
const selectRadiusPx = computeEditorRadiusPx(props.borderRadius);
|
||||
fieldStyle.borderRadius = selectRadiusPx;
|
||||
|
||||
// 체크박스/라디오 그룹도 에디터에서 padding/타이포 스타일을 일부 반영해준다.
|
||||
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
|
||||
@@ -389,14 +457,9 @@ export const computeFormOptionGroupEditorTokens = (
|
||||
fieldStyle.width = `${props.widthPx}px`;
|
||||
}
|
||||
|
||||
const radius = props.borderRadius ?? "md";
|
||||
if (radius === "none") {
|
||||
fieldStyle.borderRadius = 0;
|
||||
} else if (radius === "sm") {
|
||||
fieldStyle.borderRadius = 4;
|
||||
} else if (radius === "lg" || radius === "full") {
|
||||
fieldStyle.borderRadius = 9999;
|
||||
}
|
||||
// 모서리 둥글기: Editor 레이어에서는 lg/full 토큰을 pill 형태(9999px)로 사용한다.
|
||||
const groupRadiusPx = computeEditorRadiusPx(props.borderRadius);
|
||||
fieldStyle.borderRadius = groupRadiusPx;
|
||||
|
||||
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
|
||||
fieldStyle.paddingInline = `${props.paddingX}px`;
|
||||
@@ -535,23 +598,18 @@ export const computeFormInputPublicTokens = (props: FormInputBlockProps): FormIn
|
||||
}
|
||||
}
|
||||
|
||||
// 색상 관련 커스텀 값
|
||||
// 색상 관련 커스텀 값: builder.css 의 pb-input 기본 색상을 덮어쓰지 않도록,
|
||||
// 사용자가 커스텀 값을 설정했을 때에만 인라인 스타일을 적용한다.
|
||||
if (props.textColorCustom && props.textColorCustom.trim() !== "") {
|
||||
inputStyle.color = props.textColorCustom.trim();
|
||||
} else {
|
||||
inputStyle.color = "#f9fafb";
|
||||
}
|
||||
|
||||
if (props.fillColorCustom && props.fillColorCustom.trim() !== "") {
|
||||
inputStyle.backgroundColor = props.fillColorCustom.trim();
|
||||
} else {
|
||||
inputStyle.backgroundColor = "#020617";
|
||||
}
|
||||
|
||||
if (props.strokeColorCustom && props.strokeColorCustom.trim() !== "") {
|
||||
inputStyle.borderColor = props.strokeColorCustom.trim();
|
||||
} else {
|
||||
inputStyle.borderColor = "#334155";
|
||||
}
|
||||
|
||||
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
|
||||
@@ -629,14 +687,12 @@ export const computeFormSelectPublicTokens = (props: FormSelectBlockProps): Form
|
||||
}
|
||||
}
|
||||
|
||||
// 텍스트 색상: textColorCustom 이 설정되어 있으면 해당 색상을 사용하고, 없으면 기본 밝은 텍스트 색상을 사용한다.
|
||||
// 텍스트 색상: textColorCustom 이 설정되어 있으면 해당 색상을 사용하고,
|
||||
// 없으면 builder.css 의 pb-select 기본 색상을 그대로 사용한다.
|
||||
if (props.textColorCustom && props.textColorCustom.trim() !== "") {
|
||||
const colorValue = props.textColorCustom.trim();
|
||||
wrapperStyle.color = colorValue;
|
||||
selectStyle.color = colorValue;
|
||||
} else {
|
||||
wrapperStyle.color = "#f9fafb";
|
||||
selectStyle.color = "#f9fafb";
|
||||
}
|
||||
|
||||
// 배경/테두리 색상: fillColorCustom/strokeColorCustom 이 있으면 select 에 적용한다.
|
||||
@@ -694,6 +750,7 @@ const computeOptionGroupPublicTokensBase = (
|
||||
const textSizeClass = props.fontSizeCustom && props.fontSizeCustom.trim() !== "" ? "" : "text-xs";
|
||||
|
||||
if (widthMode === "fixed" && typeof props.widthPx === "number" && props.widthPx > 0) {
|
||||
// 퍼블릭 토큰에서는 고정 너비를 em 단위로 변환해 px 단위를 피한다.
|
||||
groupStyle.width = pxToEm(props.widthPx);
|
||||
}
|
||||
|
||||
@@ -783,7 +840,12 @@ const computeOptionGroupPublicTokensBase = (
|
||||
optionContainerStyle.borderRadius = `${radiusPx}px`;
|
||||
|
||||
if (typeof props.optionGapPx === "number" && props.optionGapPx >= 0) {
|
||||
optionsStyle.rowGap = pxToEm(props.optionGapPx);
|
||||
const gapPx = props.optionGapPx;
|
||||
const gapEm = pxToEm(gapPx);
|
||||
optionsStyle.rowGap = gapEm;
|
||||
if ((props as any).optionLayout === "inline") {
|
||||
optionsStyle.columnGap = gapEm;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -844,6 +906,10 @@ export const computeFormControllerPublicTokens = (
|
||||
.map((fieldBlock) => {
|
||||
const anyProps: any = fieldBlock.props ?? {};
|
||||
|
||||
const isRequired = Array.isArray(props.requiredFieldIds)
|
||||
? props.requiredFieldIds.includes(fieldBlock.id)
|
||||
: false;
|
||||
|
||||
if (fieldBlock.type === "formInput") {
|
||||
const name = anyProps.formFieldName ?? fieldBlock.id;
|
||||
const label = anyProps.label ?? anyProps.formFieldName ?? "입력 필드";
|
||||
@@ -854,7 +920,7 @@ export const computeFormControllerPublicTokens = (
|
||||
label,
|
||||
// 기존 PublicPageRenderer 컨트롤러 로직과 동일하게 formInput 은 항상 type "text" 로 취급한다.
|
||||
type: "text",
|
||||
required: Boolean(anyProps.required),
|
||||
required: isRequired,
|
||||
} satisfies FormControllerFieldPublicConfig;
|
||||
}
|
||||
|
||||
@@ -869,7 +935,7 @@ export const computeFormControllerPublicTokens = (
|
||||
label,
|
||||
type: "select",
|
||||
options,
|
||||
required: Boolean(anyProps.required),
|
||||
required: isRequired,
|
||||
} satisfies FormControllerFieldPublicConfig;
|
||||
}
|
||||
|
||||
@@ -886,7 +952,7 @@ export const computeFormControllerPublicTokens = (
|
||||
options,
|
||||
groupLabelMode: anyProps.groupLabelMode,
|
||||
groupLabelImageUrl: anyProps.groupLabelImageUrl,
|
||||
required: Boolean(anyProps.required),
|
||||
required: isRequired,
|
||||
} satisfies FormControllerFieldPublicConfig;
|
||||
}
|
||||
|
||||
@@ -902,23 +968,14 @@ export const computeFormControllerPublicTokens = (
|
||||
options,
|
||||
groupLabelMode: anyProps.groupLabelMode,
|
||||
groupLabelImageUrl: anyProps.groupLabelImageUrl,
|
||||
required: Boolean(anyProps.required),
|
||||
required: isRequired,
|
||||
} satisfies FormControllerFieldPublicConfig;
|
||||
})
|
||||
: [];
|
||||
|
||||
const fields: FormControllerFieldPublicConfig[] =
|
||||
hasControllerFields && controllerFields.length > 0
|
||||
? controllerFields
|
||||
: Array.isArray(props.fields) && props.fields.length > 0
|
||||
? props.fields.map((field) => ({
|
||||
id: field.id,
|
||||
name: field.name,
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
required: field.required,
|
||||
}))
|
||||
: [];
|
||||
// 프리뷰/퍼블릭 렌더러에서는 FormBlock 의 v1 fields 설정을 사용하지 않고,
|
||||
// fieldIds 로 연결된 실제 폼 필드 블록들만 기반으로 폼 필드를 렌더링한다.
|
||||
const fields: FormControllerFieldPublicConfig[] = controllerFields;
|
||||
|
||||
const mappedSubmitButton = blocks.find(
|
||||
(b) => b.type === "button" && b.id === props.submitButtonId,
|
||||
|
||||
Reference in New Issue
Block a user