테마 적용
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user