/* ============================================================================ * 서명전에 — TDS Stepper 컴포넌트 * · 여러 단계를 시각적으로 보여주는 단계 리스트. * · ProgressStepper(#142) 와 역할 구분: * ProgressStepper — "1/3, 2/3, 3/3" 처럼 진행 위치 강조. * Stepper — 절차 안내·체크리스트·가이드 카드 형태로 단계별 * 제목·설명·우측 액션을 함께 보여 주기. * * · 2 컨테이너 + 5 서브: * Stepper — 루트, children(StepperRow…) stagger 등장 모션 * StepperRow — 한 행, left / center / right 슬롯 + hideLine * StepperRow.Texts — center 슬롯 텍스트 (type A/B/C 제목·설명) * StepperRow.NumberIcon — left 숫자 아이콘 (1~9) * StepperRow.AssetFrame — left 에셋 프레임 (이미지/이모지) * StepperRow.RightArrow — right 화살표 (>) * StepperRow.RightButton — right 버튼 (TDS Button 위임) * * · Stepper props: * play (boolean, 기본 true) — 등장 애니메이션 재생 여부. * false 면 모션 없이 그대로 렌더. * delay (number, 기본 0) — 첫 행이 등장하기 전 지연 (초). * staggerDelay (number, 기본 0.1) — 행 간격 (초). * * · 등장 모션: * 각 StepperRow 에 CSS keyframe `tds-stepper-row-in` * (translateY(8px) → 0, opacity 0 → 1, 360ms cubic-bezier(0.22,1,0.36,1) both). * Stepper 가 React.cloneElement 로 자식 row 에 인덱스 + delay 변수 * (--tds-stepper-row-delay) 를 inline style 로 주입. * play=false 면 inline style 로 animation: none + opacity 1 강제. * * · 연결선(hideLine=false 기본): * 좌측 NumberIcon / AssetFrame 컬럼의 가운데에서 시작해 row bottom 까지 * grey-200 1px 세로선이 떨어져요. 마지막 행은 hideLine=true 로 끔. * * · 접근성: * Stepper 루트 role="list" + aria-label="진행 단계" (기본). * StepperRow role="listitem". * 장식적 SVG 는 모두 aria-hidden="true". * ========================================================================== */ const __STEPPER_TEXT_TYPES = { A: 1, B: 1, C: 1 }; // 에셋 프레임 모양 — Asset.frameShape preset 의 우리 측 매핑 const __STEPPER_FRAME_SHAPES = { CircleSmall: { size: 24, radius: "50%" }, CircleMedium: { size: 36, radius: "50%" }, CircleLarge: { size: 48, radius: "50%" }, CleanW24: { size: 24, radius: 0 }, CleanW32: { size: 32, radius: 0 }, RectSmall: { size: 24, radius: 6 }, RectMedium: { size: 36, radius: 8 }, RectLarge: { size: 48, radius: 10 }, }; // ============================================================================= // Stepper — 컨테이너 (children 에 stagger 등장 모션 주입) // ============================================================================= function Stepper({ children, play = true, delay = 0, staggerDelay = 0.1, className = "", style, id, "aria-label": ariaLabel, ...rest }) { // ----- props 검증 ----- const safePlay = play !== false; // truthy 만 true 로 (단, false 명시는 false) const safeDelay = typeof delay === "number" && isFinite(delay) ? Math.max(0, delay) : 0; const safeStagger = typeof staggerDelay === "number" && isFinite(staggerDelay) ? Math.max(0, staggerDelay) : 0.1; // ----- 자식 → 인덱스 + delay 주입 ----- const childArray = React.Children.toArray(children).filter(function (c) { return React.isValidElement(c); }); const total = childArray.length; const enhanced = childArray.map(function (child, idx) { const rowDelay = safeDelay + idx * safeStagger; // 자식이 이미 받은 style 을 보존하면서 inline 변수 / animation 합성 const childStyle = (child.props && child.props.style) || {}; const animStyle = safePlay ? { ...childStyle, animation: `tds-stepper-row-in 360ms cubic-bezier(0.22,1,0.36,1) both`, animationDelay: `${rowDelay}s`, } : { ...childStyle, animation: "none", opacity: 1, transform: "none", }; // hideLine 이 명시되지 않은 마지막 자식은 자동으로 hideLine=true const childProps = child.props || {}; const autoHideLine = idx === total - 1 ? true : !!childProps.hideLine; return React.cloneElement(child, { style: animStyle, hideLine: childProps.hideLine !== undefined ? childProps.hideLine : autoHideLine, __tdsStepperIsLast: idx === total - 1, }); }); const cls = ["tds-stepper", className].filter(Boolean).join(" "); return (
{enhanced}
); } // ============================================================================= // StepperRow — 한 행 (left + center + right + 연결선) // ============================================================================= function StepperRow({ left, center, right, hideLine = false, className = "", style, id, __tdsStepperIsLast, // Stepper 가 주입 — 외부 노출하지 않음 ...rest }) { // 사용자 친화 경고 if (left == null && window.console && console.warn) { console.warn("[StepperRow] left prop 이 비어있어요. 보통 NumberIcon 또는 AssetFrame 을 넣어요."); } if (center == null && window.console && console.warn) { console.warn("[StepperRow] center prop 이 비어있어요. 보통 StepperRow.Texts 를 넣어요."); } const cls = [ "tds-stepper-row", hideLine ? "tds-stepper-row--no-line" : "", className, ] .filter(Boolean) .join(" "); // 외부에 새는 건 안 좋으니 __tdsStepperIsLast 는 비표준 prop 으로 DOM 에 안 흘러가게 void __tdsStepperIsLast; return (
{left} {hideLine ? null :
{center}
{right ?
{right}
: null}
); } // ============================================================================= // StepperRow.Texts — type A/B/C 제목 + 설명 // ============================================================================= function StepperRowTexts({ type, title, description, titleProps, descriptionProps, className = "", style, ...rest }) { if (!__STEPPER_TEXT_TYPES[type]) { if (window.console && console.warn) { console.warn(`[StepperRow.Texts] type 은 "A" | "B" | "C" 만 가능해요. 받은 값: ${type}. 'A' 사용.`); } } const safeType = __STEPPER_TEXT_TYPES[type] ? type : "A"; // 사이즈 매핑 // A: title ty-5 / desc ty-6 // B: title ty-4 / desc ty-6 // C: title ty-5 / desc ty-7 const titleClass = safeType === "B" ? "ty-4 w-bold tds-stepper-text__title" : "ty-5 w-bold tds-stepper-text__title"; const descClass = safeType === "C" ? "ty-7 tds-stepper-text__desc" : "ty-6 tds-stepper-text__desc"; const tProps = titleProps || {}; const dProps = descriptionProps || {}; const cls = ["tds-stepper-text", className].filter(Boolean).join(" "); return (
{title}
{description != null && description !== "" ? (
{description}
) : null}
); } // ============================================================================= // StepperRow.NumberIcon — 1~9 숫자 아이콘 (28px 흰 원 + 파란 ring + 파란 숫자) // ============================================================================= function StepperRowNumberIcon({ number, color, bgColor, className = "", style, ...rest }) { const validNum = typeof number === "number" && number >= 1 && number <= 9 && Math.floor(number) === number; if (!validNum && window.console && console.warn) { console.warn(`[StepperRow.NumberIcon] number 는 1~9 정수만 가능해요. 받은 값: ${number}.`); } const safeNum = validNum ? number : 1; const inlineStyle = { ...(style || {}) }; if (color) inlineStyle["--tds-stepper-number-color"] = color; if (bgColor) inlineStyle["--tds-stepper-number-bg"] = bgColor; const cls = ["tds-stepper-number-icon", className].filter(Boolean).join(" "); return (
); } // ============================================================================= // StepperRow.AssetFrame — Asset.Frame 의 우리 측 시뮬레이션 (이미지/이모지 컨테이너) // ============================================================================= function StepperRowAssetFrame({ shape, content, backgroundColor, className = "", style, ...rest }) { const sp = (typeof shape === "string" && __STEPPER_FRAME_SHAPES[shape]) || null; if (!sp && window.console && console.warn) { console.warn( `[StepperRow.AssetFrame] shape 은 "CircleSmall|CircleMedium|CircleLarge|CleanW24|CleanW32|RectSmall|RectMedium|RectLarge" 중 하나여야 해요. 받은 값: ${shape}. CircleMedium 사용.` ); } const final = sp || __STEPPER_FRAME_SHAPES.CircleMedium; const inlineStyle = { width: `${final.size}px`, height: `${final.size}px`, borderRadius: typeof final.radius === "number" ? `${final.radius}px` : final.radius, background: backgroundColor && backgroundColor !== "transparent" ? backgroundColor : "transparent", ...(style || {}), }; const cls = ["tds-stepper-asset-frame", className].filter(Boolean).join(" "); return ( ); } // ============================================================================= // StepperRow.RightArrow — chevron-right 아이콘 (24px) // ============================================================================= function StepperRowRightArrow({ color, className = "", style, "aria-label": ariaLabel, ...rest }) { const inlineStyle = { ...(style || {}) }; if (color) inlineStyle["--tds-stepper-arrow-color"] = color; const cls = ["tds-stepper-right-arrow", className].filter(Boolean).join(" "); return ( ); } // ============================================================================= // StepperRow.RightButton — TDS Button 위임 (없으면 native button 폴백) // ============================================================================= function StepperRowRightButton({ children, size = "small", color = "primary", className = "", style, ...rest }) { // window.Button 이 로드돼 있으면 위임 if (typeof window !== "undefined" && typeof window.Button === "function") { const TdsButton = window.Button; return ( {children} ); } // 폴백 — TDS Button 미로드 시 const cls = [ "tds-stepper-right-button", "tds-stepper-right-button--fallback", `tds-stepper-right-button--size-${size}`, `tds-stepper-right-button--color-${color}`, className, ] .filter(Boolean) .join(" "); return ( ); } // ----- 서브 매달기 ----- StepperRow.Texts = StepperRowTexts; StepperRow.NumberIcon = StepperRowNumberIcon; StepperRow.AssetFrame = StepperRowAssetFrame; StepperRow.RightArrow = StepperRowRightArrow; StepperRow.RightButton = StepperRowRightButton; // 일부 스펙에서 Stepper.NumberIcon / Stepper.AssetFrame / Stepper.RightArrow 로 // 표기 — 동일 인스턴스를 양쪽에 매달아 두 표기 모두 동작 Stepper.Row = StepperRow; Stepper.NumberIcon = StepperRowNumberIcon; Stepper.AssetFrame = StepperRowAssetFrame; Stepper.RightArrow = StepperRowRightArrow; Stepper.RightButton = StepperRowRightButton; Stepper.Texts = StepperRowTexts; window.Stepper = Stepper; window.StepperRow = StepperRow; window.StepperRowTexts = StepperRowTexts; window.StepperRowNumberIcon = StepperRowNumberIcon; window.StepperRowAssetFrame = StepperRowAssetFrame; window.StepperRowRightArrow = StepperRowRightArrow; window.StepperRowRightButton = StepperRowRightButton; Object.assign(window, { Stepper, StepperRow, StepperRowTexts, StepperRowNumberIcon, StepperRowAssetFrame, StepperRowRightArrow, StepperRowRightButton, });