Files
page-builder/src/features/editor/utils/buttonHelpers.ts
T
jaybe 48e13d2a75
CI / pr_and_merge (push) Has been skipped
CI / test (push) Failing after 46m46s
CI / test (pull_request) Failing after 43m8s
CI / pr_and_merge (pull_request) Has been skipped
video 블록 및 리펙터링
2025-11-27 10:07:59 +09:00

250 lines
8.3 KiB
TypeScript

import type { CSSProperties } from "react";
import type { ButtonBlockProps } from "@/features/editor/state/editorStore";
export type ButtonAlign = "left" | "center" | "right" | null | undefined;
export type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
export type ButtonVariant = "solid" | "outline" | "ghost" | null | undefined;
export type ButtonColorPalette = "primary" | "muted" | "danger" | "success" | "neutral" | null | undefined;
export type ButtonRadius = "none" | "sm" | "md" | "lg" | "full" | null | undefined;
export interface ButtonStyleInput {
align?: ButtonAlign;
size?: ButtonSize;
variant?: ButtonVariant;
colorPalette?: ButtonColorPalette;
borderRadius?: ButtonRadius;
fullWidth?: boolean | null | undefined;
widthMode?: "auto" | "full" | "fixed" | null | undefined;
widthPx?: number | null | undefined;
paddingX?: number | null | undefined;
paddingY?: number | null | undefined;
fillColorCustom?: string | null | undefined;
strokeColorCustom?: string | null | undefined;
textColorCustom?: string | null | undefined;
}
export interface ButtonPbTokens {
alignClass: string;
sizeClass: string;
variantClass: string;
radiusClass: string;
inlineStyles: string[];
}
export function computeButtonPbTokens(input: ButtonStyleInput): ButtonPbTokens {
const align = input.align === "center" ? "center" : input.align === "right" ? "right" : "left";
const alignClass =
align === "center" ? "pb-text-center" : align === "right" ? "pb-text-right" : "pb-text-left";
const sizeToken = (input.size as ButtonSize | null) ?? "md";
const sizeMap: Record<Exclude<ButtonSize, null | undefined>, string> = {
xs: "pb-btn-size-xs",
sm: "pb-btn-size-sm",
md: "pb-btn-size-md",
lg: "pb-btn-size-lg",
xl: "pb-btn-size-xl",
};
const sizeClass = sizeMap[sizeToken as Exclude<ButtonSize, null | undefined>] ?? "pb-btn-size-md";
const variant = (input.variant as ButtonVariant | null) ?? "solid";
const palette = (input.colorPalette as ButtonColorPalette | null) ?? "primary";
const variantClass = `pb-btn-variant-${variant}-${palette}`;
const radiusToken = (input.borderRadius as ButtonRadius | null) ?? "md";
const radiusMap: Record<Exclude<ButtonRadius, null | undefined>, string> = {
none: "pb-btn-radius-none",
sm: "pb-btn-radius-sm",
md: "pb-btn-radius-md",
lg: "pb-btn-radius-lg",
full: "pb-btn-radius-full",
};
const radiusClass = radiusMap[radiusToken as Exclude<ButtonRadius, null | undefined>] ?? "";
const inlineStyles: string[] = [];
const widthMode = input.widthMode ?? (input.fullWidth ? "full" : "auto");
if (widthMode === "fixed" && typeof input.widthPx === "number" && input.widthPx > 0) {
inlineStyles.push(`width:${input.widthPx}px`);
}
if (typeof input.paddingX === "number") {
inlineStyles.push(`padding-left:${input.paddingX}px`);
inlineStyles.push(`padding-right:${input.paddingX}px`);
}
if (typeof input.paddingY === "number") {
inlineStyles.push(`padding-top:${input.paddingY}px`);
inlineStyles.push(`padding-bottom:${input.paddingY}px`);
}
if (input.fillColorCustom && input.fillColorCustom.trim() !== "") {
inlineStyles.push(`background-color:${input.fillColorCustom.trim()}`);
}
if (input.strokeColorCustom && input.strokeColorCustom.trim() !== "") {
inlineStyles.push(`border-color:${input.strokeColorCustom.trim()}`);
}
if (input.textColorCustom && input.textColorCustom.trim() !== "") {
inlineStyles.push(`color:${input.textColorCustom.trim()}`);
}
return {
alignClass,
sizeClass,
variantClass,
radiusClass,
inlineStyles,
};
}
// 퍼블릭 렌더러용 버튼 스타일 토큰
export interface ButtonPublicTokens {
alignClass: string;
widthClass: string;
styleOverrides: CSSProperties;
}
const pxToEm = (px: number, base = 16) => `${px / base}em`;
const convertPxStringToEm = (value?: string | null) => {
if (!value) return null;
const match = value.trim().match(/-?\d+(?:\.\d+)?/);
if (!match) return null;
const px = parseFloat(match[0]);
if (!Number.isFinite(px)) return null;
return pxToEm(px);
};
export function computeButtonPublicTokens(props: ButtonBlockProps): ButtonPublicTokens {
const align = props.align ?? "left";
const alignClass =
align === "center" ? "text-center" : align === "right" ? "text-right" : "text-left";
const styleOverrides: CSSProperties = {};
// 폰트 크기: fontSizeCustom(px)는 em 으로 변환, 그 외는 그대로 사용
const fontSizeEm = convertPxStringToEm(props.fontSizeCustom);
if (fontSizeEm) {
styleOverrides.fontSize = fontSizeEm;
}
// lineHeightCustom 은 px 단위든 아니든 그대로 사용 (기존 PublicPageRenderer 로직 유지)
if (props.lineHeightCustom && props.lineHeightCustom.trim() !== "") {
styleOverrides.lineHeight = props.lineHeightCustom;
}
// letterSpacingCustom 은 px 인 경우만 em 으로 변환, 그 외는 그대로 사용
if (props.letterSpacingCustom && props.letterSpacingCustom.trim() !== "") {
const letterSpacing = props.letterSpacingCustom.trim();
if (letterSpacing.endsWith("px")) {
const emValue = convertPxStringToEm(letterSpacing);
if (emValue) {
styleOverrides.letterSpacing = emValue;
}
} else {
styleOverrides.letterSpacing = letterSpacing;
}
}
const widthMode = props.widthMode ?? (props.fullWidth ? "full" : "auto");
if (widthMode === "fixed" && typeof props.widthPx === "number" && props.widthPx > 0) {
styleOverrides.width = pxToEm(props.widthPx);
}
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
styleOverrides.paddingInline = pxToEm(props.paddingX);
}
if (typeof props.paddingY === "number" && props.paddingY >= 0) {
styleOverrides.paddingBlock = pxToEm(props.paddingY);
}
if (props.fillColorCustom && props.fillColorCustom.trim() !== "") {
styleOverrides.backgroundColor = props.fillColorCustom;
}
if (props.strokeColorCustom && props.strokeColorCustom.trim() !== "") {
styleOverrides.borderColor = props.strokeColorCustom;
styleOverrides.borderWidth = "1px";
styleOverrides.borderStyle = "solid";
}
if (props.textColorCustom && props.textColorCustom.trim() !== "") {
styleOverrides.color = props.textColorCustom;
}
const radiusToken = props.borderRadius ?? "md";
const radiusPx =
radiusToken === "none"
? 0
: radiusToken === "sm"
? 4
: radiusToken === "lg"
? 12
: radiusToken === "full"
? 9999
: 8;
styleOverrides.borderRadius = radiusPx === 9999 ? "9999px" : pxToEm(radiusPx);
const widthClass = widthMode === "full" ? "w-full" : "";
return {
alignClass,
widthClass,
styleOverrides,
};
}
export interface ButtonEditorTokens {
wrapperWidthClass: string;
buttonStyle: CSSProperties;
}
export function computeButtonEditorTokens(props: ButtonBlockProps): ButtonEditorTokens {
const baseFullWidth = props.fullWidth ?? false;
const widthMode = props.widthMode ?? (baseFullWidth ? "full" : "auto");
const isFullWidth = widthMode === "full" || baseFullWidth;
const wrapperWidthClass = isFullWidth ? "w-full" : "inline-block";
const buttonStyle: CSSProperties = {};
if (props.fontSizeCustom && props.fontSizeCustom.trim() !== "") {
buttonStyle.fontSize = props.fontSizeCustom;
}
if (props.lineHeightCustom && props.lineHeightCustom.trim() !== "") {
buttonStyle.lineHeight = props.lineHeightCustom;
}
if (props.letterSpacingCustom && props.letterSpacingCustom.trim() !== "") {
buttonStyle.letterSpacing = props.letterSpacingCustom;
}
if (props.fillColorCustom && props.fillColorCustom.trim() !== "") {
buttonStyle.backgroundColor = props.fillColorCustom;
}
if (props.strokeColorCustom && props.strokeColorCustom.trim() !== "") {
buttonStyle.borderColor = props.strokeColorCustom;
}
if (props.textColorCustom && props.textColorCustom.trim() !== "") {
buttonStyle.color = props.textColorCustom;
}
if (widthMode === "fixed" && typeof props.widthPx === "number" && props.widthPx > 0) {
buttonStyle.width = `${props.widthPx}px`;
}
if (typeof props.paddingX === "number" && props.paddingX >= 0) {
buttonStyle.paddingInline = `${props.paddingX}px`;
}
if (typeof props.paddingY === "number" && props.paddingY >= 0) {
buttonStyle.paddingBlock = `${props.paddingY}px`;
}
return {
wrapperWidthClass,
buttonStyle,
};
}