임동욱 임동욱 08-11
refactor: 토큰 표기를 실제 이름으로 바꾸고 안내 항목을 하단 고정
토큰 표기를 `fox.color(background-default)`에서 `color-background-default`로 바꾼다.
`--fox-` 접두사만 뗀 실제 CSS 변수 이름이라 devtools에서 보는 것과 1:1로 대응하고,
함수 표기처럼 그룹명이 괄호 밖으로 빠지지 않아 목록을 훑을 때 이름이 바로 읽힌다.
표기를 만드는 함수가 하나뿐이라 스와치·서체 목록·값 표가 갈릴 일이 없다.
그 결과 쓰이지 않게 된 fn prop을 제거한다.

사이드바의 "추가 방법"은 컴포넌트 등록 여부와 무관하게 목록 맨 아래에 고정한다 —
이전에는 컴포넌트가 하나라도 생기면 사라져 추가 절차를 찾을 수 없었다. 컴포넌트가
아니라 안내이므로 점선 테두리와 흐린 글자로 등록된 탭과 층위를 구분한다.

Co-Authored-By: Claude Opus 5 
@2d38d28ea666545f75b2a4d9e22b3e4323dda168
@fox/dev-test/dev-test-page.tsx
--- @fox/dev-test/dev-test-page.tsx
+++ @fox/dev-test/dev-test-page.tsx
@@ -1,6 +1,7 @@
 "use client";
 
 import { useSyncExternalStore } from "react";
+import { cx } from "../core/utils";
 import { COMPONENT_EXAMPLES } from "./component-registry";
 import { ComponentEmpty, ComponentView } from "./component-view";
 import { ThemeSwitch } from "./theme-switch";
@@ -32,6 +33,13 @@
 
 function select(kind: string, id: string): void {
   window.location.hash = `${kind}:${id}`;
+}
+
+/** 안내 항목의 식별자 — 등록된 컴포넌트 id와 겹치지 않게 별도로 둔다. */
+const GUIDE_ID = "guide";
+
+function isRegistered(id: string): boolean {
+  return COMPONENT_EXAMPLES.some((example) => example.id === id);
 }
 
 export function DevTestPage() {
@@ -90,36 +98,43 @@
 
           <div className={styles.navGroup}>
             <h2 className={styles.navGroupTitle}>컴포넌트 · {COMPONENT_EXAMPLES.length}</h2>
-            {COMPONENT_EXAMPLES.length ? (
-              <ul className={styles.navList}>
-                {COMPONENT_EXAMPLES.map((example) => (
-                  <li key={example.id}>
-                    <button
-                      type="button"
-                      className={styles.navItem}
-                      aria-current={
-                        current.kind === "component" && current.id === example.id
-                          ? "page"
-                          : undefined
-                      }
-                      onClick={() => select("component", example.id)}
-                    >
-                      <span>{example.name}</span>
-                      <span className={styles.navCount}>{example.variants.length}</span>
-                    </button>
-                  </li>
-                ))}
-              </ul>
-            ) : (
-              <button
-                type="button"
-                className={styles.navItem}
-                aria-current={current.kind === "component" ? "page" : undefined}
-                onClick={() => select("component", "none")}
-              >
-                <span>추가 방법</span>
-              </button>
-            )}
+            <ul className={styles.navList}>
+              {COMPONENT_EXAMPLES.map((example) => (
+                <li key={example.id}>
+                  <button
+                    type="button"
+                    className={styles.navItem}
+                    aria-current={
+                      current.kind === "component" && current.id === example.id
+                        ? "page"
+                        : undefined
+                    }
+                    onClick={() => select("component", example.id)}
+                  >
+                    <span>{example.name}</span>
+                    <span className={styles.navCount}>{example.variants.length}</span>
+                  </button>
+                </li>
+              ))}
+
+              {/* 컴포넌트가 아니라 안내 항목이라 항상 목록 맨 아래에 두고 생김새를 달리한다.
+                  등록된 컴포넌트가 없을 때도 사라지지 않아야 추가 방법을 찾을 수 있다. */}
+              <li>
+                <button
+                  type="button"
+                  className={cx(styles.navItem, styles.navItemGuide)}
+                  aria-current={
+                    current.kind === "component" && !isRegistered(current.id) ? "page" : undefined
+                  }
+                  onClick={() => select("component", GUIDE_ID)}
+                >
+                  <span>추가 방법</span>
+                  <span aria-hidden="true" className={styles.navGuideMark}>
+                    +
+                  </span>
+                </button>
+              </li>
+            </ul>
           </div>
         </nav>
       </aside>
@fox/dev-test/dev-test.module.scss
--- @fox/dev-test/dev-test.module.scss
+++ @fox/dev-test/dev-test.module.scss
@@ -150,6 +150,34 @@
   opacity: 0.7;
 }
 
+// "추가 방법"은 컴포넌트가 아니라 안내다. 목록 맨 아래에 두고 점선 테두리로 구분해,
+// 등록된 컴포넌트 탭과 같은 층위로 읽히지 않게 한다.
+// `.navItem` 뒤에 와야 선택 상태(aria-current)를 덮어쓴다 — 특이도가 같아 순서가 정한다.
+.navItemGuide {
+  margin-block-start: fox.gap(2);
+  border: fox.border(1) dashed fox.color(border-neutral-strong);
+  background: none;
+  color: fox.color(font-neutral-subtler);
+
+  &:hover {
+    border-color: fox.color(border-theme-primary);
+    background: none;
+    color: fox.color(font-theme-primary);
+  }
+
+  &[aria-current="page"] {
+    border-style: solid;
+    border-color: fox.color(border-theme-primary);
+    background: none;
+    color: fox.color(font-theme-primary);
+  }
+}
+
+.navGuideMark {
+  font-size: fox.font-size(label-lg);
+  line-height: 1;
+}
+
 // ── 본문 ────────────────────────────────────────────────────────────────────
 .content {
   min-inline-size: 0;
@fox/dev-test/token-view.tsx
--- @fox/dev-test/token-view.tsx
+++ @fox/dev-test/token-view.tsx
@@ -84,6 +84,15 @@
   return `var(${token.property})`;
 }
 
+/**
+ * 화면에 표시할 토큰 식별자. `--fox-` 접두사만 뗀 **실제 이름**이라 CSS 변수
+ * (`--fox-color-background-default`)와 그대로 대응한다 — 함수 표기(`fox.color(...)`)는
+ * 그룹명이 괄호 밖으로 빠져 목록을 훑을 때 이름이 한눈에 안 들어온다.
+ */
+function label(token: TokenEntry): string {
+  return token.property.replace("--fox-", "");
+}
+
 // ── 그룹별 미리보기 ─────────────────────────────────────────────────────────
 const DEMOS: Record<string, (token: TokenEntry) => ReactNode> = {
   padding: (token) => (
@@ -148,16 +157,14 @@
 };
 
 // ── 렌더러 ──────────────────────────────────────────────────────────────────
-function Swatches({ tokens, fn }: { tokens: TokenEntry[]; fn: string }) {
+function Swatches({ tokens }: { tokens: TokenEntry[] }) {
   return (
     <div className={styles.swatchGrid}>
       {tokens.map((token) => (
         <div key={token.property} className={styles.swatch}>
           <div className={styles.swatchChip} style={{ background: v(token) }} />
           <div className={styles.swatchMeta}>
-            <span className={styles.tokenName}>
-              fox.{fn}({token.name})
-            </span>
+            <span className={styles.tokenName}>{label(token)}</span>
             <span className={styles.tokenValue}>{token.value}</span>
           </div>
         </div>
@@ -177,15 +184,13 @@
   return { fontSize: v(token) };
 }
 
-function TypeList({ tokens, fn }: { tokens: TokenEntry[]; fn: string }) {
+function TypeList({ tokens }: { tokens: TokenEntry[] }) {
   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.tokenName}>{label(token)}</span>
             <span className={styles.tokenValue}>{token.value}</span>
           </div>
           <p className={styles.typeSample} style={typeStyle(token)}>
@@ -199,11 +204,9 @@
 
 function TokenTable({
   tokens,
-  fn,
   demo,
 }: {
   tokens: TokenEntry[];
-  fn: string;
   demo?: (token: TokenEntry) => ReactNode;
 }) {
   return (
@@ -218,9 +221,7 @@
       <tbody>
         {tokens.map((token) => (
           <tr key={token.property}>
-            <td>
-              fox.{fn}({token.name})
-            </td>
+            <td>{label(token)}</td>
             <td className={styles.tdValue}>{token.value}</td>
             {demo ? <td className={styles.tdDemo}>{demo(token)}</td> : null}
           </tr>
@@ -241,10 +242,10 @@
       </h2>
       {meta?.note ? <p className={styles.sectionNote}>{meta.note}</p> : null}
 
-      {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}
+      {kind === "swatch" ? <Swatches tokens={tokens} /> : null}
+      {kind === "type" ? <TypeList tokens={tokens} /> : null}
+      {kind === "demo" ? <TokenTable tokens={tokens} demo={DEMOS[group]} /> : null}
+      {kind === "value" ? <TokenTable tokens={tokens} /> : null}
     </section>
   );
 }
Add a comment
List