import type { CSSProperties } from "react"; import type { ListBlockProps } from "@/features/editor/state/editorStore"; export interface ListExportTokens { Tag: "ul" | "ol"; items: string[]; listStyleParts: string[]; gapEm: number; } export const computeListExportTokens = (props: ListBlockProps): ListExportTokens => { const ordered = Boolean(props.ordered); const Tag: "ul" | "ol" = ordered ? "ol" : "ul"; const items: string[] = []; if (Array.isArray(props.itemsTree) && props.itemsTree.length > 0) { const walk = (nodes: { text?: string; children?: any[] }[]) => { for (const node of nodes) { if (typeof node.text === "string" && node.text.trim() !== "") { items.push(node.text); } if (Array.isArray(node.children) && node.children.length > 0) { walk(node.children); } } }; walk(props.itemsTree as any); } else if (Array.isArray(props.items)) { for (const raw of props.items) { if (typeof raw === "string" && raw.trim() !== "") { items.push(raw); } } } const alignToken = props.align; const align = alignToken === "center" ? "center" : alignToken === "right" ? "right" : "left"; const listStyleParts: string[] = [`text-align:${align}`]; const bulletStyleRaw = props.bulletStyle ?? (props.ordered ? "decimal" : "disc"); const bulletStyle = bulletStyleRaw === "none" ? "none" : bulletStyleRaw; listStyleParts.push(`list-style-type:${bulletStyle}`); const gapPx = typeof props.gapYPx === "number" ? props.gapYPx : props.gapY === "sm" ? 4 : props.gapY === "lg" ? 16 : 8; const gapEm = gapPx / 16; // 리스트 아이템 간 간격은 CSS 커스텀 프로퍼티 --pb-list-gap 으로 전달한다. // 이렇게 하면 margin-bottom 계산은 builder.css 레이어에서만 수행되어, // 정적 Export HTML 의 style 속성에 직접적인 margin-bottom 이 포함되지 않는다. listStyleParts.push(`--pb-list-gap:${gapEm}em`); if (typeof props.backgroundColorCustom === "string" && props.backgroundColorCustom.trim() !== "") { listStyleParts.push(`background-color:${props.backgroundColorCustom.trim()}`); } return { Tag, items, listStyleParts, gapEm, }; }; export interface ListEditorTokens { gapPx: number; listStyle: CSSProperties; bulletStyle: string; } export const computeListEditorTokens = (props: ListBlockProps): ListEditorTokens => { const bulletStyleRaw = props.bulletStyle ?? (props.ordered ? "decimal" : "disc"); const gapPx = typeof props.gapYPx === "number" ? props.gapYPx : props.gapY === "sm" ? 4 : props.gapY === "lg" ? 16 : 8; const listStyle: CSSProperties = {}; if (props.fontSizeCustom && props.fontSizeCustom.trim() !== "") { listStyle.fontSize = props.fontSizeCustom; } if (props.lineHeightCustom && props.lineHeightCustom.trim() !== "") { listStyle.lineHeight = props.lineHeightCustom; } if (props.textColorCustom && props.textColorCustom.trim() !== "") { listStyle.color = props.textColorCustom; } const bulletStyle = bulletStyleRaw; return { gapPx, listStyle, bulletStyle, }; }; export interface ListPublicTokens { alignClass: string; gapEm: number; listStyle: CSSProperties; bulletStyle: string; } 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 const computeListPublicTokens = (props: ListBlockProps): ListPublicTokens => { const alignClass = props.align === "center" ? "pb-text-center" : props.align === "right" ? "pb-text-right" : "pb-text-left"; const bulletStyleRaw = props.bulletStyle ?? (props.ordered ? "decimal" : "disc"); const gapPx = typeof props.gapYPx === "number" ? props.gapYPx : props.gapY === "sm" ? 4 : props.gapY === "lg" ? 16 : 8; const gapEm = gapPx / 16; const listStyle: CSSProperties = {}; if (props.fontSizeCustom && props.fontSizeCustom.trim() !== "") { const fontSizeValue = props.fontSizeCustom.trim(); if (fontSizeValue.endsWith("px")) { const fontSizeEm = convertPxStringToEm(fontSizeValue); if (fontSizeEm) { listStyle.fontSize = fontSizeEm; } } else { listStyle.fontSize = fontSizeValue; } } if (props.lineHeightCustom && props.lineHeightCustom.trim() !== "") { const lineHeightValue = props.lineHeightCustom.trim(); if (lineHeightValue.endsWith("px")) { const lineHeightEm = convertPxStringToEm(lineHeightValue); if (lineHeightEm) { listStyle.lineHeight = lineHeightEm; } } else { listStyle.lineHeight = lineHeightValue; } } if (props.textColorCustom && props.textColorCustom.trim() !== "") { listStyle.color = props.textColorCustom; } if (props.backgroundColorCustom && props.backgroundColorCustom.trim() !== "") { listStyle.backgroundColor = props.backgroundColorCustom.trim(); } // 리스트 아이템 간 여백을 CSS 변수로 노출한다. 실제 margin-bottom 은 builder.css 의 // .pb-list > li 규칙에서 --pb-list-gap 값을 참조해 계산한다. (listStyle as any)["--pb-list-gap"] = `${gapEm}em`; const bulletStyle = bulletStyleRaw; return { alignClass, gapEm, listStyle, bulletStyle, }; };