"use client"; import type { CSSProperties, ReactNode } from "react"; import type { TokenEntry } from "./read-tokens"; import styles from "./dev-test.module.scss"; /** * 토큰 **목록**은 CSSOM에서 읽으므로 여기에 적지 않는다. 이 표는 각 그룹을 어떻게 * 보여줄지와 한글 이름만 정한다. 목록에 없는 그룹이 새로 생겨도 값 표로 그려진다. * * - `swatch` 색을 칠한 카드 * - `type` 그 서체 속성을 실제로 적용한 문장 * - `demo` 그 토큰을 실제로 적용한 요소 (그룹별 렌더는 `DEMOS`) * - `value` 값만 */ export const TOKEN_GROUPS: { key: string; label: string; kind: "swatch" | "type" | "demo" | "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: "type", note: "각 토큰을 실제로 적용한 문장입니다. size 계열은 768px 경계에서 값이 바뀝니다.", }, { key: "padding", label: "패딩", kind: "demo", note: "테두리와 문단 사이의 여백이 토큰 값입니다.", }, { key: "gap", label: "간격", kind: "demo", note: "두 문단 사이의 거리가 토큰 값입니다." }, { key: "radius", label: "모서리 반경", kind: "demo" }, { key: "icon", label: "아이콘 크기", kind: "demo", note: "브라우저가 그리는 기본 체크박스를 토큰 크기로 키운 것입니다.", }, { key: "border", label: "테두리 두께", kind: "demo", note: "border-0은 값이 0이라 보이지 않습니다." }, { key: "number", label: "원시 수치", kind: "value" }, { key: "shadow", label: "그림자 수치", kind: "demo", note: "Figma에는 offset·blur 수치만 있고 완성된 그림자 토큰이 없습니다. 각 수치의 효과를 보이려고 location은 `0 값 0`, blur는 `0 0 값`으로 조합했고 색은 alpha-default-inverse25를 씁니다 — 조합 자체는 토큰이 아닙니다.", }, { key: "backdrop", label: "배경 흐림", kind: "demo", note: "오른쪽 절반에만 backdrop-filter를 걸어 원본과 대조합니다.", }, { 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 v(token: TokenEntry): string { return `var(${token.property})`; } // ── 그룹별 미리보기 ───────────────────────────────────────────────────────── const DEMOS: Record ReactNode> = { padding: (token) => (

첫 번째 문단

두 번째 문단

), gap: (token) => (

첫 번째 문단

두 번째 문단

), radius: (token) =>
, // 브라우저가 자체적으로 그리는 컨트롤이라 별도 에셋 없이 크기만 확인할 수 있다. icon: (token) => ( ), border: (token) =>
, shadow: (token) => (
), backdrop: (token) => (
{/* eslint-disable-next-line @next/next/no-img-element -- @fox는 프레임워크에 의존하지 않아야 이식된다. next/image를 쓰면 이 폴더가 Next 전용이 되고 외부 호스트마다 remotePatterns 설정도 필요해진다. */}
), }; // ── 렌더러 ────────────────────────────────────────────────────────────────── function Swatches({ tokens, fn }: { tokens: TokenEntry[]; fn: string }) { return (
{tokens.map((token) => (
fox.{fn}({token.name}) {token.value}
))}
); } /** 서체 토큰은 이름 앞머리가 곧 적용할 CSS 속성이다. */ function typeStyle(token: TokenEntry): CSSProperties { if (token.name.startsWith("family-")) { return { fontFamily: v(token) }; } if (token.name.startsWith("weight-")) { return { fontWeight: v(token) }; } return { fontSize: v(token) }; } function TypeList({ tokens, fn }: { tokens: TokenEntry[]; fn: string }) { return (
{tokens.map((token) => (
fox.{fn}({token.name}) {token.value}

다람쥐 헌 쳇바퀴에 타고파 · Handgloves 0123

))}
); } function TokenTable({ tokens, fn, demo, }: { tokens: TokenEntry[]; fn: string; demo?: (token: TokenEntry) => ReactNode; }) { return ( {demo ? : null} {tokens.map((token) => ( {demo ? : null} ))}
토큰 값미리보기
fox.{fn}({token.name}) {token.value}{demo(token)}
); } export function TokenView({ group, tokens }: { group: string; tokens: TokenEntry[] }) { const meta = TOKEN_GROUPS.find((entry) => entry.key === group); const kind = meta?.kind ?? "value"; return (

{meta?.label ?? group} · {tokens.length}

{meta?.note ?

{meta.note}

: null} {kind === "swatch" ? : null} {kind === "type" ? : null} {kind === "demo" ? : null} {kind === "value" ? : null}
); }