업종마다 템플릿이 다섯인데 넷이 "흰 바탕 + 고딕 + 둥근 모서리"에 색조만 달랐다. 고르는 화면의 미리보기도 회색 막대 세 줄 + 색 동그라미라 다섯 장이 전부 같은 그림이었다 — 뭐가 다른지 알 수 없으니 아무거나 골랐다. - shared/TemplateItem.look: 제목·본문 서체, 모서리, 테두리 두께, 그림자, 제목 자간·굵기, 섹션 여백. CSS 에 그대로 들어가는 문자열로 들고 있다 — 숫자로 두면 쓰는 쪽에서 단위를 빠뜨린 곳이 조용히 0 이 된다 - CanvasView: Tailwind v4 의 --radius-* · --shadow-* 를 캔버스 안에서만 덮는다. 변이 파일 40여 개에 흩어진 rounded-* · shadow-* 를 한 줄도 안 고치고 전부 템플릿을 따르게 된다. 배수는 Tailwind 기본 비율 그대로라 기준값 0.75rem 이면 지금까지와 픽셀 단위로 같다 - index.css: .site-canvas 는 --tpl-font-heading/body 를 이미 읽고 있었는데 아무도 넣지 않았다. 서체가 안 갈리던 진짜 이유가 이 빠진 고리다. 제목 굵기도 토큰으로 뺐다 — 간판체(Gugi)는 굵기가 한 벌뿐이라 700 을 주면 브라우저가 가짜 볼드를 씌워 획이 뭉갠다 - industryData: templatesFor() 팩토리 하나로 심플·매거진·레트로 셋. 업종은 accent 하나만 바꾼다 — 생김새는 업종이 아니라 취향의 문제다. 537줄 → 237줄 - Step4Template: 미리보기를 그 템플릿의 서체·모서리·테두리·그림자로 실제로 그린다 - index.html: Noto Serif KR 추가. 매거진 제목이 Batang 으로 떨어지는데 맥에는 그 서체가 없다 - SectionFrame: 세로 여백을 --tpl-section-space 로. 이 값 하나로 페이지의 호흡이 바뀐다 옛 템플릿 id 가 DB 에 남아 있어도 resolveTemplate 이 첫 템플릿으로 떨어뜨린다. tsc·eslint·vite build 통과(frontend·admin·site), 템플릿 12벌 look 전량 대조 + 폴백 확인
127 lines
3.9 KiB
TypeScript
127 lines
3.9 KiB
TypeScript
/**
|
|
* 섹션 제목 — 그 자체로 배리에이션을 갖는 원자.
|
|
*
|
|
* 섹션 배리에이션을 바꿔도 제목 스타일은 따로 고를 수 있어야 한다
|
|
* ("갤러리는 격자로, 제목은 가운데 정렬" 같은 조합이 실제로 나온다).
|
|
*/
|
|
import type {ReactNode} from 'react';
|
|
import type {TemplateItem} from '@o2o/shared';
|
|
import {cn} from '@/lib/utils';
|
|
|
|
export type HeadingStyle = 'underline' | 'centered' | 'eyebrow' | 'minimal';
|
|
|
|
interface SectionHeadingProps {
|
|
title: string;
|
|
subtitle?: string;
|
|
/** 제목 위에 얹는 작은 라벨. eyebrow · centered 스타일에서 쓴다. */
|
|
eyebrow?: string;
|
|
variant?: HeadingStyle;
|
|
/** 제목 오른쪽에 붙는 것(경고 배지, 추가 버튼 등). */
|
|
trailing?: ReactNode;
|
|
/**
|
|
* 템플릿 색 토큰. 넘기면 제목은 text, eyebrow 는 accent 를 따른다.
|
|
*
|
|
* ★ 부제·밑줄은 일부러 중립 회색으로 남긴다 — 여기까지 토큰을 먹이면
|
|
* accent 가 밝은 템플릿에서 부제가 배경에 묻힌다.
|
|
*/
|
|
colors?: TemplateItem['colors'];
|
|
className?: string;
|
|
}
|
|
|
|
export function SectionHeading({
|
|
title,
|
|
subtitle,
|
|
eyebrow,
|
|
variant = 'underline',
|
|
trailing,
|
|
colors,
|
|
className,
|
|
}: SectionHeadingProps) {
|
|
const titleStyle = colors ? {color: colors.text} : undefined;
|
|
const eyebrowStyle = colors ? {color: colors.accent} : undefined;
|
|
|
|
if (variant === 'centered') {
|
|
return (
|
|
<div className={cn('space-y-2 text-center', className)}>
|
|
{eyebrow && (
|
|
<span
|
|
className="text-[11px] font-semibold uppercase tracking-[0.2em] text-stone-400"
|
|
style={eyebrowStyle}
|
|
>
|
|
{eyebrow}
|
|
</span>
|
|
)}
|
|
<h2
|
|
className="tpl-title serif-title text-xl font-bold tracking-tight text-stone-900 sm:text-2xl"
|
|
style={titleStyle}
|
|
>
|
|
{title}
|
|
</h2>
|
|
{subtitle && <p className="text-xs text-stone-500 sm:text-sm">{subtitle}</p>}
|
|
{trailing && <div className="flex justify-center pt-1">{trailing}</div>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (variant === 'eyebrow') {
|
|
return (
|
|
<div className={cn('flex items-start justify-between gap-3', className)}>
|
|
<div className="space-y-1">
|
|
{eyebrow && (
|
|
<span
|
|
className="block text-[11px] font-bold uppercase tracking-wider text-amber-700"
|
|
style={eyebrowStyle}
|
|
>
|
|
{eyebrow}
|
|
</span>
|
|
)}
|
|
<h2
|
|
className="tpl-title serif-title text-xl font-bold tracking-tight text-stone-900 sm:text-2xl"
|
|
style={titleStyle}
|
|
>
|
|
{title}
|
|
</h2>
|
|
{subtitle && <p className="text-xs text-stone-500">{subtitle}</p>}
|
|
</div>
|
|
{trailing}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (variant === 'minimal') {
|
|
// 여기서 제목은 사실상 작은 라벨이라 text 가 아니라 secondary 를 쓴다.
|
|
return (
|
|
<div className={cn('flex items-baseline justify-between gap-3', className)}>
|
|
<h2
|
|
className="tpl-title text-sm font-bold uppercase tracking-[0.18em] text-stone-500"
|
|
style={colors ? {color: colors.secondary} : undefined}
|
|
>
|
|
{title}
|
|
</h2>
|
|
{trailing}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// underline — 기본. 아래 실선이 섹션의 시작을 확실히 끊어준다.
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex flex-col justify-between gap-2 border-b border-stone-200/80 pb-4 sm:flex-row sm:items-baseline',
|
|
className,
|
|
)}
|
|
>
|
|
<div>
|
|
<h2
|
|
className="tpl-title serif-title text-xl font-bold tracking-tight text-stone-900 sm:text-2xl"
|
|
style={titleStyle}
|
|
>
|
|
{title}
|
|
</h2>
|
|
{subtitle && <p className="mt-0.5 text-xs text-stone-500">{subtitle}</p>}
|
|
</div>
|
|
{trailing && <div className="self-start sm:self-auto">{trailing}</div>}
|
|
</div>
|
|
);
|
|
}
|