File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
"use client";
import type { CSSProperties } from "react";
import { useTokenRegistry } from "../_hooks/use-token-registry";
import type { TokenEntry } from "../_lib/read-tokens";
import { ThemeSwitch } from "./theme-switch";
import styles from "../design.module.scss";
/**
* 렌더 방식만 다를 뿐, 목록은 전부 CSSOM에서 읽는다 — 여기에 토큰 이름을 적지 않는다.
* `kind`는 그 그룹을 어떻게 보여줄지만 정한다: 색은 스와치, 길이는 막대, 나머지는 값만.
*/
const GROUPS: { key: string; label: string; kind: "swatch" | "bar" | "value"; note?: string }[] = [
{
key: "color",
label: "시맨틱 색상",
kind: "swatch",
note: "light/dark 값이 light-dark() 한 줄에 함께 정의돼 있습니다. 위 전환기로 두 테마를 대조해 보세요.",
},
{ key: "theme", label: "브랜드 램프", kind: "swatch", note: "primary · secondary · accent" },
{
key: "primitive",
label: "원시 팔레트",
kind: "swatch",
note: "화면에서 직접 쓰지 않는 바닥층입니다. 시맨틱 색상이 이 값들을 참조합니다.",
},
{ key: "font", label: "서체", kind: "value", note: "size 계열은 뷰포트에 따라 값이 바뀝니다." },
{ key: "padding", label: "패딩", kind: "bar" },
{ key: "gap", label: "간격", kind: "bar" },
{ key: "radius", label: "모서리 반경", kind: "bar" },
{ key: "icon", label: "아이콘 크기", kind: "bar" },
{ key: "border", label: "테두리 두께", kind: "bar" },
{ key: "number", label: "원시 수치", kind: "bar" },
{ key: "shadow", label: "그림자 수치", kind: "value" },
{ key: "backdrop", label: "배경 흐림", kind: "value" },
{ key: "form", label: "폼", kind: "value" },
{ key: "card", label: "카드", kind: "value" },
{ key: "modal", label: "모달", kind: "value" },
{ key: "grid", label: "그리드", kind: "value" },
{ key: "spacing", label: "여백", kind: "value" },
{ key: "section", label: "섹션", kind: "value" },
];
/**
* ⚠️ 이 파일은 인라인 `style`을 쓰는 **유일하게 허용되는 예외**다. 카탈로그는 토큰
* 레지스트리를 런타임에 순회하며 개수를 미리 알 수 없는 토큰을 렌더해야 하는데, CSS
* 클래스는 알 수 없는 이름에 대해 만들 수 없다. 여기서 인라인으로 넣는 값은 디자인
* 값이 아니라 **토큰 참조**(`var(--fox-*)`)이며 실제 값은 여전히 토큰이 소유한다.
* 일반 화면·컴포넌트에서는 이 패턴을 쓰지 않는다.
*/
function ref(property: keyof CSSProperties, token: TokenEntry): CSSProperties {
return { [property]: `var(${token.property})` } as CSSProperties;
}
function Swatches({ tokens, fn }: { tokens: TokenEntry[]; fn: string }) {
return (
<div className={styles.grid}>
{tokens.map((token) => (
<div key={token.property} className={styles.swatch}>
<div className={styles.swatchChip} style={ref("background", token)} />
<div className={styles.swatchMeta}>
<span className={styles.name}>
fox.{fn}({token.name})
</span>
<span className={styles.ref}>{token.value}</span>
</div>
</div>
))}
</div>
);
}
function ValueTable({
tokens,
fn,
withBar,
}: {
tokens: TokenEntry[];
fn: string;
withBar: boolean;
}) {
return (
<table className={styles.table}>
<thead>
<tr>
<th scope="col">토큰</th>
<th scope="col">값</th>
{withBar ? <th scope="col">크기</th> : null}
</tr>
</thead>
<tbody>
{tokens.map((token) => (
<tr key={token.property}>
<td>
fox.{fn}({token.name})
</td>
<td className={styles.tdValue}>{token.value}</td>
{withBar ? (
<td>
<span className={styles.bar} style={ref("inlineSize", token)} />
</td>
) : null}
</tr>
))}
</tbody>
</table>
);
}
export function TokenCatalog() {
// CSSOM은 브라우저에만 있으므로 하이드레이션 이후에 채워진다. 서버 렌더 결과가
// 비어 있어도 개발 전용 페이지라 문제되지 않는다.
const registry = useTokenRegistry();
if (!registry) {
return (
<main className={styles.page}>
<p className={styles.lede}>토큰을 읽는 중…</p>
</main>
);
}
const total = Object.values(registry).reduce((sum, list) => sum + list.length, 0);
return (
<main className={styles.page}>
<header className={styles.header}>
<div>
<h1 className={styles.title}>디자인 시스템 카탈로그</h1>
<p className={styles.lede}>
@fox/styles/tokens 전량 {total}개를 CSSOM에서 읽어 그립니다. 개발 환경에서만
열립니다. 반응형 토큰은 현재 창 크기 기준 값입니다.
</p>
</div>
<ThemeSwitch />
</header>
{GROUPS.map(({ key, label, kind, note }) => {
const tokens = registry[key];
if (!tokens?.length) {
return null;
}
return (
<section key={key} className={styles.section}>
<h2 className={styles.sectionTitle}>
{label} · {tokens.length}
</h2>
{note ? <p className={styles.sectionNote}>{note}</p> : null}
{kind === "swatch" ? (
<Swatches tokens={tokens} fn={key} />
) : (
<ValueTable tokens={tokens} fn={key} withBar={kind === "bar"} />
)}
</section>
);
})}
</main>
);
}