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
... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 |
"use client"; |
| 2 | 2 |
|
| 3 | 3 |
import { useSyncExternalStore } from "react";
|
| 4 |
+import { cx } from "../core/utils";
|
|
| 4 | 5 |
import { COMPONENT_EXAMPLES } from "./component-registry";
|
| 5 | 6 |
import { ComponentEmpty, ComponentView } from "./component-view";
|
| 6 | 7 |
import { ThemeSwitch } from "./theme-switch";
|
... | ... | @@ -32,6 +33,13 @@ |
| 32 | 33 |
|
| 33 | 34 |
function select(kind: string, id: string): void {
|
| 34 | 35 |
window.location.hash = `${kind}:${id}`;
|
| 36 |
+} |
|
| 37 |
+ |
|
| 38 |
+/** 안내 항목의 식별자 — 등록된 컴포넌트 id와 겹치지 않게 별도로 둔다. */ |
|
| 39 |
+const GUIDE_ID = "guide"; |
|
| 40 |
+ |
|
| 41 |
+function isRegistered(id: string): boolean {
|
|
| 42 |
+ return COMPONENT_EXAMPLES.some((example) => example.id === id); |
|
| 35 | 43 |
} |
| 36 | 44 |
|
| 37 | 45 |
export function DevTestPage() {
|
... | ... | @@ -90,36 +98,43 @@ |
| 90 | 98 |
|
| 91 | 99 |
<div className={styles.navGroup}>
|
| 92 | 100 |
<h2 className={styles.navGroupTitle}>컴포넌트 · {COMPONENT_EXAMPLES.length}</h2>
|
| 93 |
- {COMPONENT_EXAMPLES.length ? (
|
|
| 94 |
- <ul className={styles.navList}>
|
|
| 95 |
- {COMPONENT_EXAMPLES.map((example) => (
|
|
| 96 |
- <li key={example.id}>
|
|
| 97 |
- <button |
|
| 98 |
- type="button" |
|
| 99 |
- className={styles.navItem}
|
|
| 100 |
- aria-current={
|
|
| 101 |
- current.kind === "component" && current.id === example.id |
|
| 102 |
- ? "page" |
|
| 103 |
- : undefined |
|
| 104 |
- } |
|
| 105 |
- onClick={() => select("component", example.id)}
|
|
| 106 |
- > |
|
| 107 |
- <span>{example.name}</span>
|
|
| 108 |
- <span className={styles.navCount}>{example.variants.length}</span>
|
|
| 109 |
- </button> |
|
| 110 |
- </li> |
|
| 111 |
- ))} |
|
| 112 |
- </ul> |
|
| 113 |
- ) : ( |
|
| 114 |
- <button |
|
| 115 |
- type="button" |
|
| 116 |
- className={styles.navItem}
|
|
| 117 |
- aria-current={current.kind === "component" ? "page" : undefined}
|
|
| 118 |
- onClick={() => select("component", "none")}
|
|
| 119 |
- > |
|
| 120 |
- <span>추가 방법</span> |
|
| 121 |
- </button> |
|
| 122 |
- )} |
|
| 101 |
+ <ul className={styles.navList}>
|
|
| 102 |
+ {COMPONENT_EXAMPLES.map((example) => (
|
|
| 103 |
+ <li key={example.id}>
|
|
| 104 |
+ <button |
|
| 105 |
+ type="button" |
|
| 106 |
+ className={styles.navItem}
|
|
| 107 |
+ aria-current={
|
|
| 108 |
+ current.kind === "component" && current.id === example.id |
|
| 109 |
+ ? "page" |
|
| 110 |
+ : undefined |
|
| 111 |
+ } |
|
| 112 |
+ onClick={() => select("component", example.id)}
|
|
| 113 |
+ > |
|
| 114 |
+ <span>{example.name}</span>
|
|
| 115 |
+ <span className={styles.navCount}>{example.variants.length}</span>
|
|
| 116 |
+ </button> |
|
| 117 |
+ </li> |
|
| 118 |
+ ))} |
|
| 119 |
+ |
|
| 120 |
+ {/* 컴포넌트가 아니라 안내 항목이라 항상 목록 맨 아래에 두고 생김새를 달리한다.
|
|
| 121 |
+ 등록된 컴포넌트가 없을 때도 사라지지 않아야 추가 방법을 찾을 수 있다. */} |
|
| 122 |
+ <li> |
|
| 123 |
+ <button |
|
| 124 |
+ type="button" |
|
| 125 |
+ className={cx(styles.navItem, styles.navItemGuide)}
|
|
| 126 |
+ aria-current={
|
|
| 127 |
+ current.kind === "component" && !isRegistered(current.id) ? "page" : undefined |
|
| 128 |
+ } |
|
| 129 |
+ onClick={() => select("component", GUIDE_ID)}
|
|
| 130 |
+ > |
|
| 131 |
+ <span>추가 방법</span> |
|
| 132 |
+ <span aria-hidden="true" className={styles.navGuideMark}>
|
|
| 133 |
+ + |
|
| 134 |
+ </span> |
|
| 135 |
+ </button> |
|
| 136 |
+ </li> |
|
| 137 |
+ </ul> |
|
| 123 | 138 |
</div> |
| 124 | 139 |
</nav> |
| 125 | 140 |
</aside> |
--- @fox/dev-test/dev-test.module.scss
+++ @fox/dev-test/dev-test.module.scss
... | ... | @@ -150,6 +150,34 @@ |
| 150 | 150 |
opacity: 0.7; |
| 151 | 151 |
} |
| 152 | 152 |
|
| 153 |
+// "추가 방법"은 컴포넌트가 아니라 안내다. 목록 맨 아래에 두고 점선 테두리로 구분해, |
|
| 154 |
+// 등록된 컴포넌트 탭과 같은 층위로 읽히지 않게 한다. |
|
| 155 |
+// `.navItem` 뒤에 와야 선택 상태(aria-current)를 덮어쓴다 — 특이도가 같아 순서가 정한다. |
|
| 156 |
+.navItemGuide {
|
|
| 157 |
+ margin-block-start: fox.gap(2); |
|
| 158 |
+ border: fox.border(1) dashed fox.color(border-neutral-strong); |
|
| 159 |
+ background: none; |
|
| 160 |
+ color: fox.color(font-neutral-subtler); |
|
| 161 |
+ |
|
| 162 |
+ &:hover {
|
|
| 163 |
+ border-color: fox.color(border-theme-primary); |
|
| 164 |
+ background: none; |
|
| 165 |
+ color: fox.color(font-theme-primary); |
|
| 166 |
+ } |
|
| 167 |
+ |
|
| 168 |
+ &[aria-current="page"] {
|
|
| 169 |
+ border-style: solid; |
|
| 170 |
+ border-color: fox.color(border-theme-primary); |
|
| 171 |
+ background: none; |
|
| 172 |
+ color: fox.color(font-theme-primary); |
|
| 173 |
+ } |
|
| 174 |
+} |
|
| 175 |
+ |
|
| 176 |
+.navGuideMark {
|
|
| 177 |
+ font-size: fox.font-size(label-lg); |
|
| 178 |
+ line-height: 1; |
|
| 179 |
+} |
|
| 180 |
+ |
|
| 153 | 181 |
// ── 본문 ──────────────────────────────────────────────────────────────────── |
| 154 | 182 |
.content {
|
| 155 | 183 |
min-inline-size: 0; |
--- @fox/dev-test/token-view.tsx
+++ @fox/dev-test/token-view.tsx
... | ... | @@ -84,6 +84,15 @@ |
| 84 | 84 |
return `var(${token.property})`;
|
| 85 | 85 |
} |
| 86 | 86 |
|
| 87 |
+/** |
|
| 88 |
+ * 화면에 표시할 토큰 식별자. `--fox-` 접두사만 뗀 **실제 이름**이라 CSS 변수 |
|
| 89 |
+ * (`--fox-color-background-default`)와 그대로 대응한다 — 함수 표기(`fox.color(...)`)는 |
|
| 90 |
+ * 그룹명이 괄호 밖으로 빠져 목록을 훑을 때 이름이 한눈에 안 들어온다. |
|
| 91 |
+ */ |
|
| 92 |
+function label(token: TokenEntry): string {
|
|
| 93 |
+ return token.property.replace("--fox-", "");
|
|
| 94 |
+} |
|
| 95 |
+ |
|
| 87 | 96 |
// ── 그룹별 미리보기 ───────────────────────────────────────────────────────── |
| 88 | 97 |
const DEMOS: Record<string, (token: TokenEntry) => ReactNode> = {
|
| 89 | 98 |
padding: (token) => ( |
... | ... | @@ -148,16 +157,14 @@ |
| 148 | 157 |
}; |
| 149 | 158 |
|
| 150 | 159 |
// ── 렌더러 ────────────────────────────────────────────────────────────────── |
| 151 |
-function Swatches({ tokens, fn }: { tokens: TokenEntry[]; fn: string }) {
|
|
| 160 |
+function Swatches({ tokens }: { tokens: TokenEntry[] }) {
|
|
| 152 | 161 |
return ( |
| 153 | 162 |
<div className={styles.swatchGrid}>
|
| 154 | 163 |
{tokens.map((token) => (
|
| 155 | 164 |
<div key={token.property} className={styles.swatch}>
|
| 156 | 165 |
<div className={styles.swatchChip} style={{ background: v(token) }} />
|
| 157 | 166 |
<div className={styles.swatchMeta}>
|
| 158 |
- <span className={styles.tokenName}>
|
|
| 159 |
- fox.{fn}({token.name})
|
|
| 160 |
- </span> |
|
| 167 |
+ <span className={styles.tokenName}>{label(token)}</span>
|
|
| 161 | 168 |
<span className={styles.tokenValue}>{token.value}</span>
|
| 162 | 169 |
</div> |
| 163 | 170 |
</div> |
... | ... | @@ -177,15 +184,13 @@ |
| 177 | 184 |
return { fontSize: v(token) };
|
| 178 | 185 |
} |
| 179 | 186 |
|
| 180 |
-function TypeList({ tokens, fn }: { tokens: TokenEntry[]; fn: string }) {
|
|
| 187 |
+function TypeList({ tokens }: { tokens: TokenEntry[] }) {
|
|
| 181 | 188 |
return ( |
| 182 | 189 |
<div className={styles.typeList}>
|
| 183 | 190 |
{tokens.map((token) => (
|
| 184 | 191 |
<div key={token.property} className={styles.typeRow}>
|
| 185 | 192 |
<div className={styles.typeMeta}>
|
| 186 |
- <span className={styles.tokenName}>
|
|
| 187 |
- fox.{fn}({token.name})
|
|
| 188 |
- </span> |
|
| 193 |
+ <span className={styles.tokenName}>{label(token)}</span>
|
|
| 189 | 194 |
<span className={styles.tokenValue}>{token.value}</span>
|
| 190 | 195 |
</div> |
| 191 | 196 |
<p className={styles.typeSample} style={typeStyle(token)}>
|
... | ... | @@ -199,11 +204,9 @@ |
| 199 | 204 |
|
| 200 | 205 |
function TokenTable({
|
| 201 | 206 |
tokens, |
| 202 |
- fn, |
|
| 203 | 207 |
demo, |
| 204 | 208 |
}: {
|
| 205 | 209 |
tokens: TokenEntry[]; |
| 206 |
- fn: string; |
|
| 207 | 210 |
demo?: (token: TokenEntry) => ReactNode; |
| 208 | 211 |
}) {
|
| 209 | 212 |
return ( |
... | ... | @@ -218,9 +221,7 @@ |
| 218 | 221 |
<tbody> |
| 219 | 222 |
{tokens.map((token) => (
|
| 220 | 223 |
<tr key={token.property}>
|
| 221 |
- <td> |
|
| 222 |
- fox.{fn}({token.name})
|
|
| 223 |
- </td> |
|
| 224 |
+ <td>{label(token)}</td>
|
|
| 224 | 225 |
<td className={styles.tdValue}>{token.value}</td>
|
| 225 | 226 |
{demo ? <td className={styles.tdDemo}>{demo(token)}</td> : null}
|
| 226 | 227 |
</tr> |
... | ... | @@ -241,10 +242,10 @@ |
| 241 | 242 |
</h2> |
| 242 | 243 |
{meta?.note ? <p className={styles.sectionNote}>{meta.note}</p> : null}
|
| 243 | 244 |
|
| 244 |
- {kind === "swatch" ? <Swatches tokens={tokens} fn={group} /> : null}
|
|
| 245 |
- {kind === "type" ? <TypeList tokens={tokens} fn={group} /> : null}
|
|
| 246 |
- {kind === "demo" ? <TokenTable tokens={tokens} fn={group} demo={DEMOS[group]} /> : null}
|
|
| 247 |
- {kind === "value" ? <TokenTable tokens={tokens} fn={group} /> : null}
|
|
| 245 |
+ {kind === "swatch" ? <Swatches tokens={tokens} /> : null}
|
|
| 246 |
+ {kind === "type" ? <TypeList tokens={tokens} /> : null}
|
|
| 247 |
+ {kind === "demo" ? <TokenTable tokens={tokens} demo={DEMOS[group]} /> : null}
|
|
| 248 |
+ {kind === "value" ? <TokenTable tokens={tokens} /> : null}
|
|
| 248 | 249 |
</section> |
| 249 | 250 |
); |
| 250 | 251 |
} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?