[fix] landing: 섹션 세로 리듬 통일 — 간격을 SectionHeading 이 소유
디자인 시스템이 색·활자만 소유하고 간격은 각 섹션 파일에 흩어져 있어, 같은 관계에 값이 여러 개 생겼다. 스크롤할 때 섹션마다 호흡이 달라지던 원인. eyebrow → title 12/20px → 16px title → description 16/20/32px → 20px (display 티어 32px) 머리 → 본문 48~96px 5종 → 56px / 80px(md) 컨트롤 → 본문 56/64px → 48px 리드 → CTA 56/48px → 48px - SectionHeading 이 리듬을 소유하고 HEADING_GAP·CONTROL_GAP·CTA_GAP· DISPLAY_LEAD_GAP 를 노출. 섹션 파일에서 mb-24 같은 값을 직접 쓰지 않는다. - negotiation-demo·final-cta 가 Section/SectionHeading 을 우회해 머리를 직접 조판하고 있었다 — 아이브로우 간격이 이 둘만 20px 이던 원인. 프리미티브로 환원. - SectionHeading 이 eyebrow 없이도 빈 span 을 렌더해 유령 여백이 생기던 버그 수정. - descriptionClassName 의 text-[17px] 중복 선언 제거. lead 변형이 이미 갖고 있는 모바일 축소 단계(text-[16px] sm:text-[17px])를 지우고 있었다. - reinforcement: text-balance 가 쉼표를 넘어 끊던 헤드라인을 <br /> 로 명시. reinforcement 만 gap="none" 인데, 머리가 그리드 셀 안이라 마진이 상쇄되지 않아 items-center 정렬을 밀기 때문. 간격은 그리드 래퍼가 진다. 검증: tsc --noEmit 통과, 프로덕션 빌드 통과. DOM 실측으로 375/868/1440px 전 구간에서 머리→본문·eyebrow→title·컨트롤→본문 값 일치 확인. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
bae0da42d2
commit
55e3ab02aa
@ -3,7 +3,7 @@ import { AnimatePresence, motion } from "motion/react"
|
||||
import { Clock, Database, Handshake, Scale, TrendingUp, UserRound, type LucideIcon } from "lucide-react"
|
||||
|
||||
import { Section } from "@/components/ui/section"
|
||||
import { SectionHeading } from "@/components/ui/section-heading"
|
||||
import { CONTROL_GAP, SectionHeading } from "@/components/ui/section-heading"
|
||||
import { Typography } from "@/components/ui/typography"
|
||||
import { useFadeUp } from "@/lib/motion"
|
||||
|
||||
@ -44,18 +44,20 @@ export function Comparison() {
|
||||
|
||||
return (
|
||||
<Section id="comparison">
|
||||
<motion.div {...fadeUp} className="mb-12">
|
||||
<motion.div {...fadeUp}>
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Before & After"
|
||||
title="협상 방식을 바꾸면, 무엇이 달라질까요"
|
||||
description="기존 가격 협상의 문제는 사람이 아니라 구조입니다."
|
||||
descriptionClassName="mt-4 text-[17px] max-w-2xl"
|
||||
descriptionClassName="max-w-2xl"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* 모드 토글 — 이용 가이드 탭과 같은 세그먼트 문법 */}
|
||||
<motion.div {...toggleFadeUp} className="flex justify-center mb-14">
|
||||
{/* 모드 토글 — 이용 가이드 탭과 같은 세그먼트 문법.
|
||||
아래 간격(CONTROL_GAP)은 머리 간격보다 좁다. 토글은 자기가 조종하는
|
||||
카드들과 한 덩어리로 읽혀야지, 제목에 붙으면 무엇을 바꾸는 스위치인지 흐려진다. */}
|
||||
<motion.div {...toggleFadeUp} className={`flex justify-center ${CONTROL_GAP}`}>
|
||||
{/* 액티브를 bg-white 로 두면 트랙(bg-fill #F0F2F8) 과 거의 같은 밝기라
|
||||
멀리서 어느 쪽이 켜졌는지 안 보인다. 채움색으로 확실히 갈라준다. */}
|
||||
<div className="p-1.5 bg-fill rounded-full inline-flex gap-1.5">
|
||||
|
||||
@ -43,7 +43,6 @@ export function Contact() {
|
||||
<Section id="contact-section" bordered>
|
||||
<SectionHeading
|
||||
align="center"
|
||||
className="mb-16"
|
||||
eyebrow="Get in Touch"
|
||||
title="도입 문의"
|
||||
description="품목과 협력사 규모를 알려주시면, 예상 절감 구간과 도입 절차를 정리해 드립니다."
|
||||
|
||||
@ -12,13 +12,12 @@ export function CoreValues() {
|
||||
|
||||
return (
|
||||
<Section id="core-values">
|
||||
<motion.div {...fadeUp} className="mb-24">
|
||||
<motion.div {...fadeUp}>
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="Beyond Price"
|
||||
title="가격 그 이상의 이점"
|
||||
description="절감액은 시작일 뿐 — 관계, 투명성, 그리고 사람의 시간까지 지킵니다."
|
||||
descriptionClassName="mt-4 text-[17px]"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
|
||||
@ -14,13 +14,12 @@ export function Faq() {
|
||||
|
||||
return (
|
||||
<Section id="faq" width="sm">
|
||||
<motion.div {...fadeUp} className="mb-24">
|
||||
<motion.div {...fadeUp}>
|
||||
<SectionHeading
|
||||
align="center"
|
||||
eyebrow="FAQ"
|
||||
title="자주 묻는 질문"
|
||||
description="도입 검토에서 가장 많이 받는 질문들입니다."
|
||||
descriptionClassName="mt-4 text-[17px]"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
|
||||
@ -2,7 +2,8 @@ import { motion } from "motion/react"
|
||||
import { ArrowRight } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Typography } from "@/components/ui/typography"
|
||||
import { Section } from "@/components/ui/section"
|
||||
import { CTA_GAP, SectionHeading } from "@/components/ui/section-heading"
|
||||
import { useFadeUp } from "@/lib/motion"
|
||||
|
||||
/**
|
||||
@ -11,38 +12,44 @@ import { useFadeUp } from "@/lib/motion"
|
||||
* 이 섹션은 원래 디자인 시스템을 우회하고 있었다: bg-ink(무대 색이 아님), font-extrabold,
|
||||
* 알약 배지, blur(120px) 글로우 블롭, 다크 배경 위 라이트 토큰(text-ink-muted).
|
||||
* 전부 토큰으로 돌렸다. 글로우 블롭은 AI 가 만든 히어로의 대표 장식이라 걷어냈다.
|
||||
*
|
||||
* 섹션 껍데기와 머리도 손으로 짜던 걸 Section/SectionHeading 으로 돌렸다.
|
||||
* 우회하는 동안 아이브로우 간격이 여기만 20px 이었다(다른 섹션은 12px).
|
||||
*/
|
||||
export function FinalCTA() {
|
||||
const fadeUp = useFadeUp()
|
||||
|
||||
return (
|
||||
<section className="stage-bg text-on-stage py-24 md:py-32">
|
||||
{/* max-w-3xl 이면 "조용히 새고 있을지 / 모릅니다" 로 끊겨 마지막 줄이 고아가 된다. */}
|
||||
<div className="max-w-4xl mx-auto px-6 text-center">
|
||||
<motion.div {...fadeUp}>
|
||||
<Typography variant="eyebrow" className="block mb-5 text-primary-on-stage">
|
||||
Start Today
|
||||
</Typography>
|
||||
/* width="md"(max-w-4xl) — sm(max-w-3xl)이면 "조용히 새고 있을지 / 모릅니다" 로 끊겨
|
||||
마지막 줄이 고아가 된다. */
|
||||
<Section bg="stage" width="md" containerClassName="text-center">
|
||||
<motion.div {...fadeUp}>
|
||||
<SectionHeading
|
||||
align="center"
|
||||
tone="stage"
|
||||
size="display"
|
||||
gap="none"
|
||||
eyebrow="Start Today"
|
||||
/* 닫는 CTA 라 히어로(74px)보다는 낮추고 섹션 제목(64px)보다는 세운다. */
|
||||
titleClassName="text-[32px] sm:text-[44px] md:text-[56px]"
|
||||
title={
|
||||
<>
|
||||
<span className="text-white">지금도 구매 예산은</span>
|
||||
<br />
|
||||
<span className="text-on-stage-soft/60">조용히 새고 있을지 모릅니다</span>
|
||||
</>
|
||||
}
|
||||
description="협상 테이블에 오르지 못하고 그냥 넘어가던 건들이 있습니다. negotium 에이전트가 정해진 기준 안에서 그 건들을 대신 조율합니다."
|
||||
descriptionClassName="max-w-xl text-on-stage-soft/75"
|
||||
/>
|
||||
|
||||
<Typography variant="stageDisplay" as="h2" className="text-[32px] sm:text-[44px] md:text-[56px]">
|
||||
<span className="text-white">지금도 구매 예산은</span>
|
||||
<br />
|
||||
<span className="text-on-stage-soft/60">조용히 새고 있을지 모릅니다</span>
|
||||
</Typography>
|
||||
|
||||
<p className="mt-8 text-[16px] sm:text-[17px] text-on-stage-soft/75 max-w-xl mx-auto leading-[1.6] break-keep">
|
||||
협상 테이블에 오르지 못하고 그냥 넘어가던 건들이 있습니다. negotium 에이전트가 정해진 기준 안에서 그 건들을 대신
|
||||
조율합니다.
|
||||
</p>
|
||||
|
||||
<div className="mt-12">
|
||||
<Button href="#contact-section" variant="stage" size="xl" className="group">
|
||||
<span>도입 상담 신청</span>
|
||||
<ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
<div className={CTA_GAP}>
|
||||
<Button href="#contact-section" variant="stage" size="xl" className="group">
|
||||
<span>도입 상담 신청</span>
|
||||
<ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { useRef, useState } from "react"
|
||||
import { ArrowRight } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { CTA_GAP, DISPLAY_LEAD_GAP } from "@/components/ui/section-heading"
|
||||
import { Typography } from "@/components/ui/typography"
|
||||
import { CLUSTER_ANCHORS, DataFlowCanvas, type DataFlowPhase } from "@/components/ui/data-flow-canvas"
|
||||
|
||||
@ -55,12 +56,14 @@ export function HeroDataFlow() {
|
||||
<span className="text-on-stage-soft/60">동시에 협상합니다.</span>
|
||||
</Typography>
|
||||
|
||||
<p className="mt-11 text-base sm:text-lg text-on-stage-soft leading-relaxed break-keep max-w-2xl mx-auto">
|
||||
{/* 히어로는 h1 이어야 해서 SectionHeading(h2 고정)을 못 쓴다. 대신 간격만은
|
||||
같은 상수를 물려 최종 CTA 와 리듬이 어긋나지 않게 한다. */}
|
||||
<p className={`${DISPLAY_LEAD_GAP} text-base sm:text-lg text-on-stage-soft leading-relaxed break-keep max-w-2xl mx-auto`}>
|
||||
품목·목표가·마감일만 정하면 됩니다. 협상 에이전트가 협력사마다 따로 붙어 단가를 조율하고, 마감 시각에
|
||||
최저 투찰가로 낙찰까지 판정합니다.
|
||||
</p>
|
||||
|
||||
<div className="mt-14 flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<div className={`${CTA_GAP} flex flex-col sm:flex-row gap-3 justify-center`}>
|
||||
<Button href="#how-it-works" variant="stage" size="stageRound" className="group">
|
||||
<span>직접 협상해 보기</span>
|
||||
<ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-0.5" />
|
||||
|
||||
@ -4,7 +4,7 @@ import { Check, Monitor, Smartphone, type LucideIcon } from "lucide-react"
|
||||
|
||||
import { BrowserFrame, PhoneFrame } from "@/components/ui/device-mockups"
|
||||
import { Section } from "@/components/ui/section"
|
||||
import { SectionHeading } from "@/components/ui/section-heading"
|
||||
import { CONTROL_GAP, SectionHeading } from "@/components/ui/section-heading"
|
||||
import { Typography } from "@/components/ui/typography"
|
||||
|
||||
/** 3단계 이용 가이드 — 탭 전환식 GIF 시연 (데스크톱/모바일 목업). */
|
||||
@ -17,15 +17,14 @@ export function HowItWorksDemo() {
|
||||
<Section id="how-it-works-demo" width="lg" bordered className="overflow-hidden">
|
||||
<SectionHeading
|
||||
align="center"
|
||||
className="mb-20"
|
||||
eyebrow="How it Works"
|
||||
title="견적 생성부터 낙찰까지 한눈에 보기"
|
||||
description="견적을 열고 협상이 끝나기까지, 실제 화면으로 보여드립니다."
|
||||
descriptionClassName="text-[17px] max-w-2xl"
|
||||
descriptionClassName="max-w-2xl"
|
||||
/>
|
||||
|
||||
{/* 탭 스위치 */}
|
||||
<div className="flex justify-center mb-16">
|
||||
{/* 탭 스위치 — 비교 섹션 토글과 같은 간격 규칙(본문 쪽에 묶인다) */}
|
||||
<div className={`flex justify-center ${CONTROL_GAP}`}>
|
||||
<div className="p-1 bg-fill rounded-control inline-flex flex-wrap justify-center gap-1">
|
||||
{DEMO_TABS.map((tab) => (
|
||||
<button
|
||||
|
||||
@ -3,6 +3,8 @@ import { motion } from "motion/react"
|
||||
import { ArrowRight, RotateCcw } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Section } from "@/components/ui/section"
|
||||
import { SectionHeading } from "@/components/ui/section-heading"
|
||||
import { Typography } from "@/components/ui/typography"
|
||||
import { EASE_OUT_EXPO } from "@/lib/motion"
|
||||
import {
|
||||
@ -122,20 +124,24 @@ export function NegotiationDemo() {
|
||||
const sliderMax = role === "seller" ? item.listPrice : item.marketLow
|
||||
|
||||
return (
|
||||
<section id="how-it-works" className="stage-bg text-on-stage py-24 md:py-32">
|
||||
<div className="max-w-5xl mx-auto px-6">
|
||||
<div className="text-center mb-14 md:mb-20">
|
||||
<Typography variant="eyebrow" className="block mb-5 text-primary-on-stage">
|
||||
Try it Yourself
|
||||
</Typography>
|
||||
{/* 두 문장은 의미 단위가 다르므로 줄을 명시적으로 나눈다.
|
||||
자동 줄바꿈에 맡기면 "에이전트는 기준 / 밖으로 안 나갑니다" 처럼 끊긴다. */}
|
||||
<Typography variant="title" as="h2">
|
||||
<span className="text-white">직접 협상해 보세요.</span>
|
||||
<br />
|
||||
<span className="text-on-stage-soft/60">에이전트는 정해진 기준을 벗어나지 않습니다.</span>
|
||||
</Typography>
|
||||
</div>
|
||||
<Section id="how-it-works" bg="stage" width="lg">
|
||||
{/* 머리는 손으로 짜지 않고 SectionHeading 을 쓴다. 예전엔 여기서 직접 조판하느라
|
||||
아이브로우 간격만 20px(다른 섹션은 12px)이었다 — 프리미티브를 우회하면
|
||||
같은 관계에 두 번째 값이 생긴다. */}
|
||||
{/* 두 문장은 의미 단위가 다르므로 줄을 명시적으로 나눈다.
|
||||
자동 줄바꿈에 맡기면 "에이전트는 기준 / 밖으로 안 나갑니다" 처럼 끊긴다. */}
|
||||
<SectionHeading
|
||||
align="center"
|
||||
tone="stage"
|
||||
eyebrow="Try it Yourself"
|
||||
title={
|
||||
<>
|
||||
<span className="text-white">직접 협상해 보세요.</span>
|
||||
<br />
|
||||
<span className="text-on-stage-soft/60">에이전트는 정해진 기준을 벗어나지 않습니다.</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* AnimatePresence mode="wait" 를 쓰지 않는다. 나가는 애니메이션이 끝나야 다음
|
||||
화면이 마운트돼서 클릭 후 0.8초가 빈다 — 역할 선택처럼 즉시 반응해야 하는
|
||||
@ -335,8 +341,7 @@ export function NegotiationDemo() {
|
||||
<p className="mt-12 text-center text-[13px] text-on-stage-soft/40">
|
||||
실제 판정 규칙(앵커링가·협상 카드 3장·개찰)을 그대로 옮긴 데모입니다. 금액은 예시입니다.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { motion } from "motion/react"
|
||||
import { GitMerge, RefreshCw, Scale, type LucideIcon } from "lucide-react"
|
||||
|
||||
import { Section } from "@/components/ui/section"
|
||||
import { SectionHeading } from "@/components/ui/section-heading"
|
||||
import { HEADING_GAP, SectionHeading } from "@/components/ui/section-heading"
|
||||
import { Typography } from "@/components/ui/typography"
|
||||
import { OrbitRing } from "@/components/ui/orbit-ring"
|
||||
import { useFadeUp } from "@/lib/motion"
|
||||
@ -15,13 +15,25 @@ export function Reinforcement() {
|
||||
<Section id="reinforcement" bg="stage" width="lg">
|
||||
{/* 순환 링은 원래 히어로에 있었는데, 루프가 의미상 속하는 자리는 여기다 —
|
||||
"협상할수록 좋아진다"를 말하는 섹션이고, 히어로는 이미 세로 예산이 포화였다. */}
|
||||
<motion.div {...fadeUp} className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-center mb-20">
|
||||
{/* 머리가 그리드 셀 안에 들어가 있어서 여기만 gap="none" 이다. 그리드 아이템은
|
||||
독립 서식 문맥이라 마진이 상쇄되지 않고, 머리에 mb 를 걸면 items-center 의
|
||||
세로 정렬을 밀어버린다. 섹션 간격은 그리드 래퍼가 대신 진다. */}
|
||||
<motion.div {...fadeUp} className={`grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-center ${HEADING_GAP}`}>
|
||||
<div className="lg:col-span-7">
|
||||
<SectionHeading
|
||||
className="text-center lg:text-left"
|
||||
gap="none"
|
||||
tone="stage"
|
||||
eyebrow="Reinforcement Learning"
|
||||
title="협상할수록, 더 좋은 조건으로"
|
||||
/* 쉼표에서 줄을 명시적으로 끊는다. break-keep + text-balance 에 맡기면
|
||||
두 줄 길이를 맞추려고 "협상할수록, 더 / 좋은 조건으로" 처럼 쉼표를
|
||||
넘어가서 끊어진다 — 조건절과 결과절이 한 줄에 섞여 의미가 흐려진다. */
|
||||
title={
|
||||
<>
|
||||
협상할수록,
|
||||
<br />더 좋은 조건으로
|
||||
</>
|
||||
}
|
||||
description={
|
||||
<>
|
||||
잘 깎는 요령은 담당자 머릿속에만 남고, 결과는 그날 컨디션에 흔들립니다. negotium은 지난 거래 기록과
|
||||
@ -29,7 +41,6 @@ export function Reinforcement() {
|
||||
받아들여지는 선인지를 데이터로 익혀서, 결렬 없이 도달 가능한 최선을 찾아갑니다.
|
||||
</>
|
||||
}
|
||||
descriptionClassName="mt-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:col-span-5 flex justify-center">
|
||||
|
||||
@ -24,7 +24,6 @@ export function ROISimulator() {
|
||||
<Section id="benchmarks" width="lg" bordered className="border-b border-line">
|
||||
<SectionHeading
|
||||
align="center"
|
||||
className="mb-24"
|
||||
eyebrow="ROI Simulation"
|
||||
title={
|
||||
<>
|
||||
@ -38,7 +37,7 @@ export function ROISimulator() {
|
||||
연간 구매 예산과 공급사 수만 입력해 보수적인 시뮬레이션 성과를 즉시 시각화해 보세요.
|
||||
</>
|
||||
}
|
||||
descriptionClassName="mt-4 text-[17px] max-w-2xl"
|
||||
descriptionClassName="max-w-2xl"
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-start">
|
||||
|
||||
@ -10,7 +10,45 @@ import { cn } from "@/lib/utils"
|
||||
* 레퍼런스에서는 영문 대문자라 조판 요소로 기능하지만, 한글은 대문자가 없어서
|
||||
* 그냥 "작은 글씨"가 되고 일곱 번 반복되면 벽지가 된다.
|
||||
* 위계는 라벨이 아니라 헤드라인 크기(본문의 3.5~4배)와 2톤 대비로 만든다.
|
||||
*
|
||||
* ── 세로 리듬 ────────────────────────────────────────────────────────
|
||||
* 머리 안팎의 간격은 전부 이 파일이 소유한다. 섹션 파일에서 각자 mb-12 / mb-16 /
|
||||
* mb-20 / mb-24 를 붙이던 걸 걷어낸 결과다. 같은 관계(머리 → 본문)에 값이 다섯 개
|
||||
* 있으면 스크롤할 때 섹션마다 호흡이 달라지고, 그게 "정돈이 안 됐다"는 인상의
|
||||
* 실제 원인이었다.
|
||||
*
|
||||
* 규칙은 하나다 — 간격이 위계를 만든다. 가까울수록 한 덩어리로 읽힌다.
|
||||
* 값이 흔들리면 "어디까지가 한 덩어리인가"가 섹션마다 달라져 보인다.
|
||||
*
|
||||
* eyebrow → title 16px 라벨은 제목에 붙는다
|
||||
* title → description 20 / 32px 큰 활자는 더 벌린다. 64px 제목 아래 20px 은 붙어 보인다
|
||||
* 머리 → 본문 56 / 80px 섹션 안에서 가장 큰 끊김
|
||||
*
|
||||
* 섹션 파일에서 이 관계들에 mb-· mt- 유틸리티를 직접 쓰지 않는다. 필요하면 아래 상수를
|
||||
* import 해서 쓰고, 새 값이 필요하다고 느껴지면 여기에 이름을 붙여서 추가한다.
|
||||
* 눈대중으로 한 칸씩 조정하기 시작하면 다시 지금 상태로 돌아온다.
|
||||
*/
|
||||
|
||||
/** 머리 → 본문. 머리를 직접 조판하는 섹션(무대 데모)도 이 상수를 쓴다. */
|
||||
export const HEADING_GAP = "mb-14 md:mb-20"
|
||||
/** 탭·토글 행 → 본문. 머리 간격보다 좁아서 컨트롤이 위가 아니라 본문 쪽에 묶인다. */
|
||||
export const CONTROL_GAP = "mb-12"
|
||||
/** 리드 문단 → 버튼 행 (히어로·최종 CTA). */
|
||||
export const CTA_GAP = "mt-12"
|
||||
/** display 티어 제목 → 리드 문단. 히어로는 h1 이라 SectionHeading 을 못 쓰고 이 상수를 직접 쓴다. */
|
||||
export const DISPLAY_LEAD_GAP = "mt-8"
|
||||
|
||||
/*
|
||||
* 제목 조판 티어. 티어가 바뀌면 리드 문단까지의 간격도 같이 움직인다 —
|
||||
* Typography 주석의 "크기·굵기·조임은 같이 움직인다"와 같은 이유로, 간격도 딸려간다.
|
||||
*/
|
||||
const SIZES = {
|
||||
/** 섹션 표준 h2 (34 / 64px) */
|
||||
title: { variant: "title", descriptionGap: "mt-5" },
|
||||
/** 히어로·최종 CTA 급 디스플레이 (38~74px) */
|
||||
display: { variant: "stageDisplay", descriptionGap: DISPLAY_LEAD_GAP },
|
||||
} as const
|
||||
|
||||
type SectionHeadingProps = {
|
||||
eyebrow?: string
|
||||
title: React.ReactNode
|
||||
@ -18,8 +56,14 @@ type SectionHeadingProps = {
|
||||
align?: "left" | "center"
|
||||
/** 다크 무대 섹션(bg="stage")에서는 "stage" 를 준다. Typography 변형이 먹색을 물고 있어서 필요하다. */
|
||||
tone?: "light" | "stage"
|
||||
/** 제목 조판 티어. display 는 히어로·최종 CTA 전용. */
|
||||
size?: keyof typeof SIZES
|
||||
/** 머리 아래 표준 간격을 끈다. 뒤에 컨트롤 행이 붙는 섹션에서만 쓴다. */
|
||||
gap?: "heading" | "none"
|
||||
className?: string
|
||||
/** 리드 문단 폭 제한 등 (예: "max-w-2xl") */
|
||||
/** 제목 크기 미세조정 (예: 최종 CTA 의 축소 디스플레이) */
|
||||
titleClassName?: string
|
||||
/** 리드 문단 폭 제한·색 조정 (예: "max-w-2xl"). 세로 간격은 여기서 주지 않는다. */
|
||||
descriptionClassName?: string
|
||||
}
|
||||
|
||||
@ -29,23 +73,31 @@ function SectionHeading({
|
||||
description,
|
||||
align = "left",
|
||||
tone = "light",
|
||||
size = "title",
|
||||
gap = "heading",
|
||||
className,
|
||||
titleClassName,
|
||||
descriptionClassName,
|
||||
}: SectionHeadingProps) {
|
||||
const onStage = tone === "stage"
|
||||
const { variant, descriptionGap } = SIZES[size]
|
||||
|
||||
return (
|
||||
<div className={cn(align === "center" && "text-center", className)}>
|
||||
<Typography variant="eyebrow" className={cn("mb-3 block", onStage && "text-primary-on-stage")}>
|
||||
{eyebrow}
|
||||
</Typography>
|
||||
<Typography variant="title" className={cn(onStage && "text-on-stage")}>
|
||||
<div className={cn(align === "center" && "text-center", gap === "heading" && HEADING_GAP, className)}>
|
||||
{/* eyebrow 가 없을 때 빈 span 을 렌더하면 mb + 30px 행간만큼 유령 여백이 생긴다.
|
||||
지금은 모든 섹션이 eyebrow 를 넘기지만, 하나만 빼도 그 섹션만 제목이 내려앉는다. */}
|
||||
{eyebrow && (
|
||||
<Typography variant="eyebrow" className={cn("mb-4 block", onStage && "text-primary-on-stage")}>
|
||||
{eyebrow}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant={variant} as="h2" className={cn(onStage && "text-on-stage", titleClassName)}>
|
||||
{title}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography
|
||||
variant="lead"
|
||||
className={cn("mt-5", align === "center" && "mx-auto", onStage && "text-on-stage-soft", descriptionClassName)}
|
||||
className={cn(descriptionGap, align === "center" && "mx-auto", onStage && "text-on-stage-soft", descriptionClassName)}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user