/* ============================================================================ * 서명전에 — TDS ListHeader 컴포넌트 * · 페이지/섹션 상단에 [제목 ─ 우측 액션] + [보조 설명] 을 배치하는 헤더. * · 제목은 항상 자식으로 서브 컴포넌트를 받음 — Title* 셋: * ListHeader.TitleParagraph — 일반 텍스트 * ListHeader.TitleTextButton — 클릭 가능한 텍스트 버튼 (clear/arrow/underline) * ListHeader.TitleSelector — 드롭다운 화살표가 붙은 셀렉터 * · 우측 액션도 서브 컴포넌트: * ListHeader.RightText — 단순 보조 텍스트 * ListHeader.RightArrow — 텍스트 + 화살표, 클릭 가능 * · 보조 설명: * ListHeader.DescriptionParagraph — 작은 회색 텍스트 * descriptionPosition='top'(기본) / 'bottom' 으로 위/아래 위치 조정. * * · props (ListHeaderProps): * title* ReactNode — Title* 서브 컴포넌트 권장 * titleWidthRatio number — 제목 영역 비율 (기본 0.66, [0.2, 0.8] 클램프) * description ReactNode — DescriptionParagraph 권장 * descriptionPosition "top" | "bottom" (기본 "top") * right ReactNode — RightText / RightArrow 권장 * rightAlignment "center" | "bottom" (기본 "center") * className, style, aria-* ... 루트 패스스루 * ========================================================================== */ /* ────────────────────────────────────────────────────────────────────────── * 타이포 유틸 — TDS 의 "t4|t5|t6|t7" 을 우리의 .ty-4 / .ty-5 / ... 로 매핑 * · 우리 토큰은 ty-1 ~ ty-7 (메인) 와 sub-ty-1 ~ sub-ty-13 (서브) 로 나뉨. * · TDS 가 명시한 "t*" 는 Main 7 과 1:1 매칭됨 (t4 = ty-4, ...). * ────────────────────────────────────────────────────────────────────────── */ function __lhTypoClass(typo) { if (typeof typo !== "string") return ""; const m = typo.match(/^t(\d+)$/i); if (!m) return ""; const n = parseInt(m[1], 10); if (n < 1 || n > 7) return ""; return `ty-${n}`; } /* xsmall/medium/large → 우리 ty- 매핑 (TDS TextButton 사이즈) */ const __LH_BTN_SIZE_TO_TY = { xsmall: "ty-7", medium: "ty-6", large: "ty-5", }; const __LH_FW_CLASS = { regular: "w-regular", medium: "w-medium", bold: "w-bold", semibold: "w-semibold", }; /* ────────────────────────────────────────────────────────────────────────── * Chevron SVG 헬퍼 — IOSIcon 이 떠있으면 위임, 아니면 inline SVG 폴백 * ────────────────────────────────────────────────────────────────────────── */ function __lhChevron({ direction = "down", size = 14, color }) { if (typeof window !== "undefined" && typeof window.IOSIcon === "function") { const _IOSIcon = window.IOSIcon; const name = direction === "right" ? "chev-right" : direction === "down" ? "chev-down" : direction === "up" ? "chev-up" : "chev-right"; return ( <_IOSIcon name={name} size={size} /> ); } // Fallback: inline path const d = direction === "right" ? "M9 6l6 6-6 6" : direction === "down" ? "M6 9l6 6 6-6" : direction === "up" ? "M18 15l-6-6-6 6" : "M9 6l6 6-6 6"; return ( ); } /* ────────────────────────────────────────────────────────────────────────── * Title.Paragraph — 일반 제목 텍스트 * ────────────────────────────────────────────────────────────────────────── */ function ListHeaderTitleParagraph({ typography, fontWeight, color, children, className = "", style, ...rest }) { if (!typography) { if (window.console && console.warn) { console.warn("[ListHeader.TitleParagraph] `typography` 는 필수예요. 't4' | 't5' | 't7' 중 하나를 주세요."); } } if (!fontWeight) { if (window.console && console.warn) { console.warn("[ListHeader.TitleParagraph] `fontWeight` 는 필수예요. 'bold' | 'medium' | 'regular' 중 하나를 주세요."); } } const cls = [ "tds-listheader__title-para", __lhTypoClass(typography), __LH_FW_CLASS[fontWeight] || "", className, ].filter(Boolean).join(" "); const mergedStyle = { ...(style || {}) }; if (color) mergedStyle.color = color; return

{children}

; } /* ────────────────────────────────────────────────────────────────────────── * Title.Selector — 드롭다운 셀렉터 * ────────────────────────────────────────────────────────────────────────── */ function ListHeaderTitleSelector({ typography, fontWeight, color, onClick, disabled = false, children, className = "", style, ...rest }) { if (!typography) { if (window.console && console.warn) { console.warn("[ListHeader.TitleSelector] `typography` 는 필수예요. 't4' | 't5' | 't7' 중 하나를 주세요."); } } const cls = [ "tds-listheader__title-selector", __lhTypoClass(typography), __LH_FW_CLASS[fontWeight] || "w-bold", className, ].filter(Boolean).join(" "); const mergedStyle = { ...(style || {}) }; if (color) mergedStyle.color = color; return ( ); } /* ────────────────────────────────────────────────────────────────────────── * Title.TextButton — 클릭 가능한 텍스트 버튼 (variant clear/arrow/underline) * ────────────────────────────────────────────────────────────────────────── */ function ListHeaderTitleTextButton({ size, fontWeight, variant = "underline", color, onClick, disabled = false, htmlType, // 임시 — TDS spec 상 추후 제거 예정 children, className = "", style, ...rest }) { if (!size) { if (window.console && console.warn) { console.warn("[ListHeader.TitleTextButton] `size` 는 필수예요. 'xsmall' | 'medium' | 'large' 중 하나를 주세요."); } } if (!fontWeight) { if (window.console && console.warn) { console.warn("[ListHeader.TitleTextButton] `fontWeight` 는 필수예요. 'bold' | 'medium' | 'regular' 중 하나를 주세요."); } } const validVariants = { clear: 1, arrow: 1, underline: 1 }; if (!validVariants[variant]) { if (window.console && console.warn) { console.warn(`[ListHeader.TitleTextButton] \`variant\` 는 'clear' | 'arrow' | 'underline' 중 하나여야 해요. 받은 값: ${variant}`); } } const safeVariant = validVariants[variant] ? variant : "underline"; const cls = [ "tds-listheader__title-button", `tds-listheader__title-button--variant-${safeVariant}`, __LH_BTN_SIZE_TO_TY[size] || "ty-6", __LH_FW_CLASS[fontWeight] || "w-bold", className, ].filter(Boolean).join(" "); const mergedStyle = { ...(style || {}) }; if (color) mergedStyle.color = color; return ( ); } /* ────────────────────────────────────────────────────────────────────────── * DescriptionParagraph — 보조 설명 * ────────────────────────────────────────────────────────────────────────── */ function ListHeaderDescriptionParagraph({ children, className = "", style, ...rest }) { const cls = ["tds-listheader__description", className].filter(Boolean).join(" "); return

{children}

; } /* ────────────────────────────────────────────────────────────────────────── * RightText — 우측 보조 텍스트 * ────────────────────────────────────────────────────────────────────────── */ function ListHeaderRightText({ typography, color, children, className = "", style, ...rest }) { if (!typography) { if (window.console && console.warn) { console.warn("[ListHeader.RightText] `typography` 는 필수예요. 't6' | 't7' 중 하나를 주세요."); } } const cls = [ "tds-listheader__right-text", __lhTypoClass(typography), className, ].filter(Boolean).join(" "); const mergedStyle = { ...(style || {}) }; if (color) mergedStyle.color = color; return

{children}

; } /* ────────────────────────────────────────────────────────────────────────── * RightArrow — 우측 텍스트 + 화살표 (onClick 가능) * ────────────────────────────────────────────────────────────────────────── */ function ListHeaderRightArrow({ typography, color, // 화살표 아이콘 색 textColor, // 텍스트 색 onClick, children, className = "", style, ...rest }) { if (!typography) { if (window.console && console.warn) { console.warn("[ListHeader.RightArrow] `typography` 는 필수예요. 't6' | 't7' 중 하나를 주세요."); } } const clickable = typeof onClick === "function"; const cls = [ "tds-listheader__right-arrow", __lhTypoClass(typography), className, ].filter(Boolean).join(" "); const mergedStyle = { ...(style || {}) }; if (textColor) mergedStyle.color = textColor; // 클릭 가능하면 button, 아니면 div const Tag = clickable ? "button" : "div"; const tagProps = clickable ? { type: "button", onClick } : {}; return ( {children != null && children !== "" && {children}} {__lhChevron({ direction: "right", size: 14, color })} ); } /* ────────────────────────────────────────────────────────────────────────── * 메인 ListHeader * ────────────────────────────────────────────────────────────────────────── */ function ListHeader({ title, titleWidthRatio = 0.66, description, descriptionPosition = "top", right, rightAlignment = "center", className = "", style, ...rest }) { if (title == null) { if (window.console && console.warn) { console.warn("[ListHeader] `title` 은 필수예요. ListHeader.TitleParagraph / TitleSelector / TitleTextButton 중 하나를 넣어 주세요."); } } // 비율 클램프 — 의미 없는 0/1 같은 값 방어 let ratio = typeof titleWidthRatio === "number" ? titleWidthRatio : 0.66; if (!isFinite(ratio)) ratio = 0.66; if (ratio < 0.2) ratio = 0.2; if (ratio > 0.8) ratio = 0.8; const validPositions = { top: 1, bottom: 1 }; const safePosition = validPositions[descriptionPosition] ? descriptionPosition : "top"; const validAlignments = { center: 1, bottom: 1 }; const safeAlignment = validAlignments[rightAlignment] ? rightAlignment : "center"; const cls = ["tds-listheader", className].filter(Boolean).join(" "); const rowCls = [ "tds-listheader__row", `tds-listheader__row--align-${safeAlignment}`, ].join(" "); const titleSlot = (
{title}
); const rightSlot = right != null ? (
{right}
) : null; const descriptionSlot = description != null ? ( typeof description === "string" ? {description} : description ) : null; return (
{descriptionSlot && safePosition === "top" && descriptionSlot}
{titleSlot} {rightSlot}
{descriptionSlot && safePosition === "bottom" && descriptionSlot}
); } ListHeader.TitleParagraph = ListHeaderTitleParagraph; ListHeader.TitleSelector = ListHeaderTitleSelector; ListHeader.TitleTextButton = ListHeaderTitleTextButton; ListHeader.DescriptionParagraph = ListHeaderDescriptionParagraph; ListHeader.RightText = ListHeaderRightText; ListHeader.RightArrow = ListHeaderRightArrow; window.ListHeader = ListHeader; Object.assign(window, { ListHeader });