임동욱 임동욱 08-11
feat: 토큰 미리보기를 실제 적용 형태로 렌더
값만 보여주던 막대를 걷어내고 그룹별로 실제 적용 결과를 보인다 — 서체는 그 속성을
적용한 문장, 패딩·간격은 문단 두 개 사이의 공간, 반경·테두리는 실제 테두리를 가진
div, 아이콘은 브라우저 기본 체크박스, 그림자는 실제 box-shadow, 배경 흐림은 사진 위
절반에만 건 backdrop-filter다. 원시 수치는 값만 남긴다.

그림자는 Figma에 offset·blur 수치만 있고 조합과 색이 정의돼 있지 않다. 각 수치의
효과를 보이려고 location은 `0 값 0`, blur는 `0 0 값`으로 걸고 색은 실재 토큰인
alpha-default-inverse25를 쓴다 — 조합 자체는 토큰이 아니며 섹션 설명에도 적었다.

배경 흐림 예제 이미지는 next/image가 아니라 다. @fox가 Next에 의존하면 폴더째
복사되는 이식성이 깨지고 외부 호스트마다 remotePatterns 설정이 필요해진다.

Co-Authored-By: Claude Opus 5 
@f5129c5b378bf6ef5c665f21e7e9b7494f7660d7
@fox/dev-test/dev-test.module.scss
--- @fox/dev-test/dev-test.module.scss
+++ @fox/dev-test/dev-test.module.scss
@@ -254,14 +254,126 @@
   color: fox.color(font-neutral-subtle);
 }
 
-// 크기 토큰을 눈으로 비교하기 위한 막대. 폭은 인라인 style이 토큰을 참조해 넣는다.
-.bar {
-  display: block;
-  block-size: fox.icon(2);
-  border-radius: fox.radius(1);
+// 미리보기 칸은 내용만큼만 차지하게 두어 토큰/값 열이 밀리지 않게 한다.
+.tdDemo {
+  inline-size: 1%;
+  white-space: nowrap;
+}
+
+// ── 서체 미리보기 ───────────────────────────────────────────────────────────
+.typeList {
+  display: flex;
+  flex-direction: column;
+}
+
+.typeRow {
+  display: flex;
+  flex-wrap: wrap;
+  gap: fox.gap(2) fox.gap(7);
+  align-items: baseline;
+  padding-block: fox.padding(5);
+  border-block-end: fox.border(1) solid fox.color(border-neutral-subtle);
+}
+
+.typeMeta {
+  display: flex;
+  flex: 0 0 22rem;
+  flex-direction: column;
+  gap: fox.gap(1);
+}
+
+.typeSample {
+  min-inline-size: 20rem;
+  flex: 1;
+  margin: 0;
+  color: fox.color(font-neutral-default);
+}
+
+// ── 패딩 · 간격 미리보기 ────────────────────────────────────────────────────
+// 점선 테두리와 옅은 바탕이 "토큰이 만든 공간"이고, 흰 칩이 문단이다.
+.demoPadBox,
+.demoGapBox {
+  border: fox.border(1) dashed fox.color(border-theme-primary);
   background: fox.color(surface-theme-primary);
 }
 
+.demoPadBox {
+  display: inline-block;
+}
+
+.demoGapBox {
+  display: inline-flex;
+  align-items: center;
+}
+
+.demoText {
+  margin: 0;
+  padding: fox.padding(1) fox.padding(3);
+  background: fox.color(surface-neutral-default);
+  color: fox.color(font-neutral-default);
+  font-size: fox.font-size(label-xsm);
+  white-space: nowrap;
+}
+
+// ── 모서리 반경 · 테두리 두께 미리보기 ──────────────────────────────────────
+.demoRadius,
+.demoBorder {
+  inline-size: fox.icon(7);
+  block-size: fox.icon(7);
+  border-color: fox.color(border-theme-primary);
+  border-style: solid;
+  background: fox.color(surface-neutral-gray);
+}
+
+.demoRadius {
+  border-width: fox.border(2);
+}
+
+// ── 아이콘 크기 미리보기 ────────────────────────────────────────────────────
+// 브라우저 기본 체크박스를 토큰 크기로 키운다 — 별도 에셋 없이 크기만 비교한다.
+.demoIcon {
+  display: block;
+  margin: 0;
+  accent-color: fox.color(surface-theme-primary);
+}
+
+// ── 그림자 미리보기 ─────────────────────────────────────────────────────────
+// 그림자 색은 Figma에 토큰이 없어 중립 알파 토큰을 데모용으로 쓴다.
+// 인라인 style이 참조할 수 있도록 여기서 커스텀 프로퍼티로 내려둔다.
+.demoShadow {
+  --demo-shadow-color: #{fox.color(alpha-default-inverse25)};
+
+  inline-size: fox.icon(7);
+  block-size: fox.icon(6);
+  border-radius: fox.radius(2);
+  background: fox.color(surface-neutral-default);
+}
+
+// ── 배경 흐림 미리보기 ──────────────────────────────────────────────────────
+// 캔버스 크기는 디자인 값이 아니라 데모용 지면이라 토큰을 두지 않는다.
+.demoBackdrop {
+  position: relative;
+  overflow: hidden;
+  inline-size: 20rem;
+  block-size: 12rem;
+  border-radius: fox.radius(2);
+}
+
+.demoBackdropImage {
+  display: block;
+  inline-size: 100%;
+  block-size: 100%;
+  object-fit: cover;
+}
+
+.demoBackdropOverlay {
+  position: absolute;
+  inset-block: 0;
+  inset-inline-end: 0;
+  inline-size: 50%;
+  border-inline-start: fox.border(1) solid fox.color(border-neutral-overlay);
+}
+
 // ── 컴포넌트 예제 ───────────────────────────────────────────────────────────
 .variantList {
   display: flex;
@fox/dev-test/token-view.tsx
--- @fox/dev-test/token-view.tsx
+++ @fox/dev-test/token-view.tsx
@@ -1,18 +1,22 @@
 "use client";
 
-import type { CSSProperties } from "react";
+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" | "bar" | "value";
+  kind: "swatch" | "type" | "demo" | "value";
   note?: string;
 }[] = [
   {
@@ -31,17 +35,37 @@
   {
     key: "font",
     label: "서체",
-    kind: "value",
-    note: "size 계열은 768px 경계에서 값이 바뀝니다.",
+    kind: "type",
+    note: "각 토큰을 실제로 적용한 문장입니다. size 계열은 768px 경계에서 값이 바뀝니다.",
   },
-  { 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: "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" },
@@ -56,16 +80,80 @@
  * 넣는 값은 디자인 값이 아니라 **토큰 참조**(`var(--fox-*)`)이며 실제 값은 여전히
  * 토큰이 소유한다. 일반 컴포넌트에서는 이 패턴을 쓰지 않는다.
  */
-function ref(property: keyof CSSProperties, token: TokenEntry): CSSProperties {
-  return { [property]: `var(${token.property})` } as CSSProperties;
+function v(token: TokenEntry): string {
+  return `var(${token.property})`;
 }
 
+// ── 그룹별 미리보기 ─────────────────────────────────────────────────────────
+const DEMOS: Record<string, (token: TokenEntry) => ReactNode> = {
+  padding: (token) => (
+    <div className={styles.demoPadBox} style={{ padding: v(token) }}>
+      <p className={styles.demoText}>첫 번째 문단</p>
+      <p className={styles.demoText}>두 번째 문단</p>
+    </div>
+  ),
+
+  gap: (token) => (
+    <div className={styles.demoGapBox} style={{ gap: v(token) }}>
+      <p className={styles.demoText}>첫 번째 문단</p>
+      <p className={styles.demoText}>두 번째 문단</p>
+    </div>
+  ),
+
+  radius: (token) => <div className={styles.demoRadius} style={{ borderRadius: v(token) }} />,
+
+  // 브라우저가 자체적으로 그리는 컨트롤이라 별도 에셋 없이 크기만 확인할 수 있다.
+  icon: (token) => (
+    <input
+      type="checkbox"
+      defaultChecked
+      aria-label={`icon-${token.name} 크기 미리보기`}
+      className={styles.demoIcon}
+      style={{ inlineSize: v(token), blockSize: v(token) }}
+    />
+  ),
+
+  border: (token) => <div className={styles.demoBorder} style={{ borderWidth: v(token) }} />,
+
+  shadow: (token) => (
+    <div
+      className={styles.demoShadow}
+      style={{
+        boxShadow: token.name.startsWith("blur")
+          ? `0 0 ${v(token)} var(--demo-shadow-color)`
+          : `0 ${v(token)} 0 var(--demo-shadow-color)`,
+      }}
+    />
+  ),
+
+  backdrop: (token) => (
+    <div className={styles.demoBackdrop}>
+      {/* eslint-disable-next-line @next/next/no-img-element -- @fox는 프레임워크에 의존하지
+          않아야 이식된다. next/image를 쓰면 이 폴더가 Next 전용이 되고 외부 호스트마다
+          remotePatterns 설정도 필요해진다. */}
+      <img
+        src="https://picsum.photos/seed/picsum/200/300"
+        alt=""
+        className={styles.demoBackdropImage}
+      />
+      <div
+        className={styles.demoBackdropOverlay}
+        style={{
+          backdropFilter: `blur(${v(token)})`,
+          WebkitBackdropFilter: `blur(${v(token)})`,
+        }}
+      />
+    </div>
+  ),
+};
+
+// ── 렌더러 ──────────────────────────────────────────────────────────────────
 function Swatches({ tokens, fn }: { tokens: TokenEntry[]; fn: string }) {
   return (
     <div className={styles.swatchGrid}>
       {tokens.map((token) => (
         <div key={token.property} className={styles.swatch}>
-          <div className={styles.swatchChip} style={ref("background", token)} />
+          <div className={styles.swatchChip} style={{ background: v(token) }} />
           <div className={styles.swatchMeta}>
             <span className={styles.tokenName}>
               fox.{fn}({token.name})
@@ -78,14 +166,45 @@
   );
 }
 
-function ValueTable({
+/** 서체 토큰은 이름 앞머리가 곧 적용할 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 (
+    <div className={styles.typeList}>
+      {tokens.map((token) => (
+        <div key={token.property} className={styles.typeRow}>
+          <div className={styles.typeMeta}>
+            <span className={styles.tokenName}>
+              fox.{fn}({token.name})
+            </span>
+            <span className={styles.tokenValue}>{token.value}</span>
+          </div>
+          <p className={styles.typeSample} style={typeStyle(token)}>
+            다람쥐 헌 쳇바퀴에 타고파 · Handgloves 0123
+          </p>
+        </div>
+      ))}
+    </div>
+  );
+}
+
+function TokenTable({
   tokens,
   fn,
-  withBar,
+  demo,
 }: {
   tokens: TokenEntry[];
   fn: string;
-  withBar: boolean;
+  demo?: (token: TokenEntry) => ReactNode;
 }) {
   return (
     <table className={styles.table}>
@@ -93,7 +212,7 @@
         <tr>
           <th scope="col">토큰</th>
           <th scope="col">값</th>
-          {withBar ? <th scope="col">크기</th> : null}
+          {demo ? <th scope="col">미리보기</th> : null}
         </tr>
       </thead>
       <tbody>
@@ -103,11 +222,7 @@
               fox.{fn}({token.name})
             </td>
             <td className={styles.tdValue}>{token.value}</td>
-            {withBar ? (
-              <td>
-                <span className={styles.bar} style={ref("inlineSize", token)} />
-              </td>
-            ) : null}
+            {demo ? <td className={styles.tdDemo}>{demo(token)}</td> : null}
           </tr>
         ))}
       </tbody>
@@ -125,11 +240,11 @@
         {meta?.label ?? group} · {tokens.length}
       </h2>
       {meta?.note ? <p className={styles.sectionNote}>{meta.note}</p> : null}
-      {kind === "swatch" ? (
-        <Swatches tokens={tokens} fn={group} />
-      ) : (
-        <ValueTable tokens={tokens} fn={group} withBar={kind === "bar"} />
-      )}
+
+      {kind === "swatch" ? <Swatches tokens={tokens} fn={group} /> : null}
+      {kind === "type" ? <TypeList tokens={tokens} fn={group} /> : null}
+      {kind === "demo" ? <TokenTable tokens={tokens} fn={group} demo={DEMOS[group]} /> : null}
+      {kind === "value" ? <TokenTable tokens={tokens} fn={group} /> : null}
     </section>
   );
 }
Add a comment
List