/* ============================================================================
* 서명전에 — TDS Top 컴포넌트
* · 페이지 최상단 영역 — 헤더/타이틀 블록을 일관되게 짜는 빌더.
* 여러 슬롯(upper / 중앙 [subtitleTop+title+subtitleBottom] / right / lower)을
* 조합해서 서비스 곳곳의 진입 헤더(예: HistoryAccount 첫 줄, Result 결과 헤더,
* Help/FAQ 카테고리 헤더, Onboarding 안내 헤더)를 한 줄로 짤 수 있어요.
*
* · 구조:
* ┌──────────────────────────────────────────────┐
* │ upperGap (기본 24px) │
* ├──────────────────────────────────────────────┤
* │ upper (Asset/Lottie 등) │
* │ │
* │ ┌──────────────────────┬───────────────────┐ │
* │ │ subtitleTop │ right │ │
* │ │ title │ (Asset/Button) │ │
* │ │ subtitleBottom │ │ │
* │ └──────────────────────┴───────────────────┘ │
* │ │
* │ lower (LowerButton / LowerCTA) │
* ├──────────────────────────────────────────────┤
* │ lowerGap (기본 24px) │
* └──────────────────────────────────────────────┘
*
* · 서브 (총 14):
* Top.TitleParagraph — h1 텍스트 (role="heading" aria-level="1" 자동)
* Top.TitleTextButton — 타이틀이 텍스트 버튼처럼 동작 (TextButton 위임)
* Top.TitleSelector — 타이틀이 셀렉터 (chev-down, aria-haspopup="listbox")
* Top.SubtitleParagraph — h2 부제목 (role="heading" aria-level="2" 자동)
* Top.SubtitleTextButton — 부제목이 텍스트 버튼처럼 동작
* Top.SubtitleSelector — 부제목이 셀렉터 (chev-down, aria-haspopup="listbox")
* Top.SubtitleBadges — 부제목 자리에 뱃지 묶음 (Badge 위임)
* Top.UpperAssetContent — upper 슬롯 Asset 래퍼 (정렬 + 여백 규칙)
* Top.RightAssetContent — right 슬롯 Asset 래퍼
* Top.RightButton — right 슬롯 버튼 (Button 위임, size 기본 medium)
* Top.RightArrow — right 슬롯 chev-right 아이콘 단독
* Top.LowerButton — lower 작은 버튼 1개 (Button 위임, size 기본 small)
* Top.LowerCTA — lower 두 버튼 (type="2-button"; left/right 버튼 슬롯)
* Top.LowerCTAButton — LowerCTA 안에 들어가는 버튼 (Button 위임, size 기본 large)
*
* · 여백 규칙:
* - upperGap / lowerGap 은 number (px) — `${n}px` 인라인.
* - subtitleTop ↔ title : 6px 마진. title ↔ subtitleBottom : 6px 마진.
* - upper 슬롯과 본문 사이 16px (UpperAssetContent 가 자체 margin-bottom).
* - right 슬롯은 본문 텍스트 column 의 우측. 기본 vertical center,
* rightVerticalAlign="end" 면 아래쪽 정렬(긴 본문에 작게 맞춤).
* - lower 슬롯과 본문 사이 16px.
*
* · 접근성:
* - TitleParagraph 는 자동으로 role="heading" + aria-level="1".
* - SubtitleParagraph 는 자동으로 role="heading" + aria-level="2".
* - TitleSelector / SubtitleSelector 는 자동으로 aria-haspopup="listbox".
* - 호출자가 같은 prop 을 따로 주면 호출자 값이 우선(오버라이드 가능).
*
* · 의존: Button, TextButton, Paragraph, Badge — 미로드 시 native 폴백 + 폴백 클래스.
* ========================================================================== */
// =============================================================================
// 보조 — chev SVG (셀렉터 / RightArrow)
// =============================================================================
function __TopChevDown({ size = 16, color, ariaHidden = true }) {
// 위에서 아래로 펼치는 셀렉터 — 16x16 viewbox
return (
);
}
function __TopChevRight({ size = 20, color, ariaHidden = true, ariaLabel }) {
return (
);
}
// =============================================================================
// 보조 — typography 힌트 (TDS t3/st2 ↔ 우리 ty-3/sub-ty-2 등)
// =============================================================================
function __topTypoForTitleSize(size) {
// 22 → t3 / 28 → st2 (스펙)
if (size === 28) return "st2";
return "t3";
}
function __topTypoForSubtitleSize(size) {
// 13 → t7 / 15 → t6 / 17 → t5 (스펙)
if (size === 13) return "t7";
if (size === 15) return "t6";
return "t5";
}
function __topFwForSubtitleSize(size) {
// 13/15 → regular / 17 → medium (스펙)
if (size === 13 || size === 15) return "regular";
return "medium";
}
// =============================================================================
// Root — Top
// =============================================================================
function Top({
title,
upperGap = 24,
lowerGap = 24,
upper,
lower,
subtitleTop,
subtitleBottom,
right,
rightVerticalAlign = "center",
className = "",
style,
...rest
}) {
const cls = [
"tds-top",
rightVerticalAlign === "end" ? "tds-top--right-end" : "tds-top--right-center",
className,
]
.filter(Boolean)
.join(" ");
// upperGap / lowerGap 은 number → `${n}px`. 음수/NaN 은 0 으로.
const safeNum = function (v, fallback) {
if (typeof v !== "number" || !isFinite(v) || v < 0) return fallback;
return v;
};
const padTop = safeNum(upperGap, 24);
const padBottom = safeNum(lowerGap, 24);
const mergedStyle = Object.assign(
{
paddingTop: `${padTop}px`,
paddingBottom: `${padBottom}px`,
},
style || {}
);
return (
{upper ?
{upper}
: null}
{/* 본문 — 좌측 텍스트 컬럼 + 우측 슬롯 */}
{(title || subtitleTop || subtitleBottom || right) ? (
{subtitleTop ?
{subtitleTop}
: null}
{title ?
{title}
: null}
{subtitleBottom ?
{subtitleBottom}
: null}
{right ?
{right}
: null}
) : null}
{lower ?
{lower}
: null}
);
}
// =============================================================================
// Top.TitleParagraph
// =============================================================================
function TopTitleParagraph({
size = 22,
color,
typography,
fontWeight = "bold",
className = "",
children,
// 접근성 자동 부여 — 호출자가 명시하면 그쪽 우선
role,
"aria-level": ariaLevel,
...rest
}) {
// size 검증 — 22 / 28 만
if (size !== 22 && size !== 28) {
if (window.console && console.warn) {
console.warn(`[Top.TitleParagraph] size 는 22 | 28 만 가능. 받은 값: ${size}. 22 폴백.`);
}
size = 22;
}
const tg = typography || __topTypoForTitleSize(size);
// 색은 인라인으로 주입(Paragraph 는 color prop 지원). 기본 grey-800.
const effectiveColor = color || "var(--tds-grey-800)";
const cls = ["tds-top__title-paragraph", className].filter(Boolean).join(" ");
// Paragraph 컴포넌트 위임 — 미로드 시 폴백
if (typeof window.Paragraph === "function") {
return (
{children}
);
}
return (
{children}
);
}
// =============================================================================
// Top.TitleTextButton — 타이틀이 텍스트 버튼처럼 동작
// =============================================================================
function TopTitleTextButton({
size = "xlarge",
variant,
color,
className = "",
children,
...rest
}) {
const cls = ["tds-top__title-text-button", className].filter(Boolean).join(" ");
const effectiveColor = color || "var(--tds-grey-800)";
if (typeof window.TextButton === "function") {
return (
{children}
);
}
// 폴백 — TextButton 미로드 시 native button
return (
);
}
// =============================================================================
// Top.TitleSelector — 타이틀이 셀렉터 (chev-down + aria-haspopup="listbox")
// =============================================================================
function TopTitleSelector({
color,
typography,
fontWeight = "bold",
className = "",
children,
// 접근성 자동
role,
"aria-level": ariaLevel,
"aria-haspopup": ariaHaspopup,
onClick,
...rest
}) {
// 타이틀 셀렉터는 spec 상 size 별 토큰 — typography 만 있고 size 가 없는 형태.
// 우리는 ty-3 (=22 라인) 을 기본으로 두고, 호출자가 typography 로 덮어쓸 수 있게.
const tg = typography || "t3";
const effectiveColor = color || "var(--tds-grey-800)";
const cls = ["tds-top__title-selector", className].filter(Boolean).join(" ");
return (
);
}
// =============================================================================
// Top.SubtitleParagraph
// =============================================================================
function TopSubtitleParagraph({
size = 17,
color,
typography,
fontWeight,
className = "",
children,
role,
"aria-level": ariaLevel,
...rest
}) {
if (size !== 13 && size !== 15 && size !== 17) {
if (window.console && console.warn) {
console.warn(`[Top.SubtitleParagraph] size 는 13 | 15 | 17 만 가능. 받은 값: ${size}. 17 폴백.`);
}
size = 17;
}
const tg = typography || __topTypoForSubtitleSize(size);
const fw = fontWeight || __topFwForSubtitleSize(size);
const effectiveColor = color || "var(--tds-grey-700)";
const cls = ["tds-top__subtitle-paragraph", className].filter(Boolean).join(" ");
if (typeof window.Paragraph === "function") {
return (
{children}
);
}
return (
{children}
);
}
// =============================================================================
// Top.SubtitleTextButton
// =============================================================================
function TopSubtitleTextButton({
size = "medium",
variant = "arrow",
color,
className = "",
children,
...rest
}) {
const cls = ["tds-top__subtitle-text-button", className].filter(Boolean).join(" ");
const effectiveColor = color || "var(--tds-grey-700)";
if (typeof window.TextButton === "function") {
return (
{children}
);
}
return (
);
}
// =============================================================================
// Top.SubtitleSelector — 부제목이 셀렉터 (chev-down + aria-haspopup="listbox")
// =============================================================================
function TopSubtitleSelector({
size = 17,
color,
typography,
fontWeight,
type = "arrow", // "arrow" 면 chev-down 표시, 그 외(none 등) 면 표시 안 함
className = "",
children,
"aria-haspopup": ariaHaspopup,
onClick,
...rest
}) {
if (size !== 13 && size !== 15 && size !== 17) {
if (window.console && console.warn) {
console.warn(`[Top.SubtitleSelector] size 는 13 | 15 | 17 만 가능. 받은 값: ${size}. 17 폴백.`);
}
size = 17;
}
const tg = typography || __topTypoForSubtitleSize(size);
const fw = fontWeight || __topFwForSubtitleSize(size);
const effectiveColor = color || "var(--tds-grey-700)";
// chev 크기 — size 와 1:1 (16/14/12)
const chevPx = size === 13 ? 14 : size === 15 ? 16 : 18;
const cls = ["tds-top__subtitle-selector", className].filter(Boolean).join(" ");
return (
);
}
// =============================================================================
// Top.SubtitleBadges
// =============================================================================
function TopSubtitleBadges({
badges,
className = "",
...rest
}) {
if (!Array.isArray(badges)) {
if (window.console && console.warn) {
console.warn("[Top.SubtitleBadges] badges 는 배열이 필요해요.");
}
return null;
}
const cls = ["tds-top__subtitle-badges", className].filter(Boolean).join(" ");
return (
{badges.map(function (b, i) {
if (!b || typeof b !== "object") return null;
// 별칭 정규화 — TDS spec 의 type/style 도 받음
const color = b.color || b.type || "blue";
const variant = b.variant || b.style || "weak";
const text = b.text;
const key = b.key != null ? b.key : `top-badge-${i}`;
if (typeof window.Badge === "function") {
return (
{text}
);
}
return (
{text}
);
})}
);
}
// =============================================================================
// Top.UpperAssetContent — upper 슬롯 Asset 래퍼 (여백 규칙)
// =============================================================================
function TopUpperAssetContent({
content,
className = "",
...rest
}) {
const cls = ["tds-top__upper-asset", className].filter(Boolean).join(" ");
return (
{content}
);
}
// =============================================================================
// Top.RightAssetContent — right 슬롯 Asset 래퍼
// =============================================================================
function TopRightAssetContent({
content,
className = "",
...rest
}) {
const cls = ["tds-top__right-asset", className].filter(Boolean).join(" ");
return (
{content}
);
}
// =============================================================================
// Top.RightButton — Button 위임 (size 기본 medium)
// =============================================================================
function TopRightButton({
size = "medium",
color = "primary",
variant,
className = "",
children,
...rest
}) {
const cls = ["tds-top__right-button", className].filter(Boolean).join(" ");
if (typeof window.Button === "function") {
return (
{children}
);
}
return (
);
}
// =============================================================================
// Top.RightArrow — chev-right 아이콘 단독 (Icon 위임 자리에 inline SVG)
// =============================================================================
function TopRightArrow({
color = "var(--tds-grey-600)",
size = 20,
name, // 호환용 — 무시되지만 받아둠
className = "",
...rest
}) {
const cls = ["tds-top__right-arrow", className].filter(Boolean).join(" ");
return (
<__TopChevRight size={size} color={color} />
);
}
// =============================================================================
// Top.LowerButton — 작은 보조 버튼 1개 (size 기본 small)
// =============================================================================
function TopLowerButton({
size = "small",
color = "primary",
variant = "weak",
display = "inline",
className = "",
children,
...rest
}) {
const cls = ["tds-top__lower-button", className].filter(Boolean).join(" ");
if (typeof window.Button === "function") {
return (
{children}
);
}
return (
);
}
// =============================================================================
// Top.LowerCTA — 두 버튼 가로 배치 (type="2-button")
// =============================================================================
function TopLowerCTA({
type = "2-button",
leftButton,
rightButton,
className = "",
...rest
}) {
if (type !== "2-button") {
if (window.console && console.warn) {
console.warn(`[Top.LowerCTA] type 은 "2-button" 만 지원해요. 받은 값: ${type}.`);
}
}
const cls = ["tds-top__lower-cta", `tds-top__lower-cta--${type}`, className]
.filter(Boolean)
.join(" ");
return (
{leftButton}
{rightButton}
);
}
// =============================================================================
// Top.LowerCTAButton — LowerCTA 안 버튼 (size 기본 large)
// =============================================================================
function TopLowerCTAButton({
size = "large",
color = "primary",
variant,
display = "block",
className = "",
children,
...rest
}) {
const cls = ["tds-top__lower-cta-button", className].filter(Boolean).join(" ");
if (typeof window.Button === "function") {
return (
{children}
);
}
return (
);
}
// =============================================================================
// 서브 매달기
// =============================================================================
Top.TitleParagraph = TopTitleParagraph;
Top.TitleTextButton = TopTitleTextButton;
Top.TitleSelector = TopTitleSelector;
Top.SubtitleParagraph = TopSubtitleParagraph;
Top.SubtitleTextButton = TopSubtitleTextButton;
Top.SubtitleSelector = TopSubtitleSelector;
Top.SubtitleBadges = TopSubtitleBadges;
Top.UpperAssetContent = TopUpperAssetContent;
Top.RightAssetContent = TopRightAssetContent;
Top.RightButton = TopRightButton;
Top.RightArrow = TopRightArrow;
Top.LowerButton = TopLowerButton;
Top.LowerCTA = TopLowerCTA;
Top.LowerCTAButton = TopLowerCTAButton;
window.Top = Top;
Object.assign(window, { Top });