테마 적용
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
@@ -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) {