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, 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] ?? "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, 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] ?? ""; 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, }; }