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 { typoPresetNames, type TokenEntry } from "../_lib/read-tokens";
import { ThemeSwitch } from "./theme-switch";
import styles from "../design.module.scss";
/**
* ⚠️ 이 파일은 인라인 `style`을 쓰는 **유일하게 허용되는 예외**다. 카탈로그는 토큰
* 레지스트리를 런타임에 순회하며 개수를 미리 알 수 없는 토큰을 렌더해야 하는데, CSS
* 클래스는 알 수 없는 이름에 대해 만들 수 없다. 여기서 인라인으로 넣는 값은 디자인
* 값이 아니라 **토큰 참조**(`var(--fox-*)`)이며 실제 값은 여전히 토큰이 소유한다.
* 일반 화면·컴포넌트에서는 이 패턴을 쓰지 않는다.
*/
function ref(property: keyof CSSProperties, token: TokenEntry): CSSProperties {
return { [property]: `var(${token.property})` } as CSSProperties;
}
function typoRef(preset: string): CSSProperties {
return {
fontSize: `var(--fox-typo-${preset}-size)`,
fontWeight: `var(--fox-typo-${preset}-weight)`,
lineHeight: `var(--fox-typo-${preset}-line-height)`,
letterSpacing: `var(--fox-typo-${preset}-letter-spacing)`,
};
}
function ValueTable({ rows }: { rows: { label: string; value: string }[] }) {
return (
<table className={styles.table}>
<thead>
<tr>
<th scope="col">토큰</th>
<th scope="col">값</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.label}>
<td>{row.label}</td>
<td>{row.value}</td>
</tr>
))}
</tbody>
</table>
);
}
function rows(group: TokenEntry[] | undefined, fn: string) {
return (group ?? []).map((token) => ({
label: `fox.${fn}(${token.name})`,
value: token.value,
}));
}
export function TokenCatalog() {
// CSSOM은 브라우저에만 있으므로 하이드레이션 이후에 채워진다. 서버 렌더 결과가
// 비어 있어도 개발 전용 페이지라 문제되지 않는다.
const registry = useTokenRegistry();
if (!registry) {
return (
<main className={styles.page}>
<p className={styles.lede}>토큰을 읽는 중…</p>
</main>
);
}
const colors = registry.color ?? [];
const spaces = registry.space ?? [];
const radii = registry.radius ?? [];
const shadows = registry.shadow ?? [];
const presets = typoPresetNames(registry);
return (
<main className={styles.page}>
<header className={styles.header}>
<div>
<h1 className={styles.title}>디자인 시스템 카탈로그</h1>
<p className={styles.lede}>
@fox/styles/tokens 전량을 CSSOM에서 읽어 그립니다. 개발 환경에서만 열립니다.
</p>
</div>
<ThemeSwitch />
</header>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>색상 · {colors.length}</h2>
<p className={styles.sectionNote}>
라이트/다크 값이 light-dark() 한 줄에 함께 정의돼 있습니다. 위 전환기로 두 테마를
대조해 보세요.
</p>
<div className={styles.grid}>
{colors.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.color({token.name})</span>
<span className={styles.ref}>{token.value}</span>
</div>
</div>
))}
</div>
</section>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>타이포 프리셋 · {presets.length}</h2>
<p className={styles.sectionNote}>
크기·굵기·행간·자간 4종이 한 묶음입니다. @include fox.typo(이름)으로 적용합니다.
</p>
{presets.map((preset) => (
<div key={preset} className={styles.typoRow}>
<span className={styles.typoName}>{preset}</span>
<span className={styles.typoSample} style={typoRef(preset)}>
다람쥐 헌 쳇바퀴에 타고파 · Handgloves 0123
</span>
</div>
))}
</section>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>간격 · {spaces.length}</h2>
<p className={styles.sectionNote}>
4px 배수 스케일. 1rem = 10px이므로 fox.space(4) = 1.6rem = 16px입니다.
</p>
{spaces.map((token) => (
<div key={token.property} className={styles.spaceRow}>
<span className={styles.typoName}>
space({token.name}) · {token.value}
</span>
<span className={styles.spaceBar} style={ref("inlineSize", token)} />
</div>
))}
</section>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>모서리 반경 · {radii.length}</h2>
<div className={styles.grid}>
{radii.map((token) => (
<div key={token.property} className={styles.tile} style={ref("borderRadius", token)}>
{token.name} · {token.value}
</div>
))}
</div>
</section>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>그림자 · {shadows.length}</h2>
<p className={styles.sectionNote}>크기가 아니라 쓰임(고도)으로 이름 붙였습니다.</p>
<div className={styles.grid}>
{shadows.map((token) => (
<div key={token.property} className={styles.shadowCell}>
<div className={styles.shadowTile} style={ref("boxShadow", token)}>
{token.name}
</div>
</div>
))}
</div>
</section>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>모션</h2>
<ValueTable
rows={[...rows(registry.duration, "duration"), ...rows(registry.easing, "easing")]}
/>
</section>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>서체 · 굵기 · z 층 · 브레이크포인트</h2>
<p className={styles.sectionNote}>
브레이크포인트는 미디어 쿼리가 var()를 해석하지 못하므로 실제 사용은 fox.media()
믹스인 경유입니다. 또한 미디어 쿼리 안의 rem은 html font-size의 영향을 받지 않아
48rem은 480px가 아니라 768px입니다.
</p>
<ValueTable
rows={[
...rows(registry.font, "font"),
...rows(registry.weight, "weight"),
...rows(registry.z, "z"),
...rows(registry.breakpoint, "media"),
]}
/>
</section>
</main>
);
}