refactor: 토큰 매니페스트 제거
값의 출처가 컴파일된 CSS로 옮겨간 뒤 매니페스트가 제공하던 것은 Figma 변수 경로와 "원본에선 참조였으나 컬렉션이 없어 값으로 굳은 것"(chart-alt 18개) 표시 둘뿐이었다. 둘 다 쓰지 않기로 해 생성물·화면·변환기에서 모두 걷어낸다. 변환기는 이제 SCSS만 생성한다. 화면은 스타일시트 외에 아무것도 참조하지 않는다. Co-Authored-By: Claude Opus 5
@faaf03ebef2abc08ec70b06ad09faa2fe2d6f841
--- @fox/dev-test/dev-test.module.scss
+++ @fox/dev-test/dev-test.module.scss
... | ... | @@ -252,24 +252,12 @@ |
| 252 | 252 |
word-break: break-all; |
| 253 | 253 |
} |
| 254 | 254 |
|
| 255 |
-// Figma 변수 경로 — 변수 페이지와 눈으로 맞추기 위한 보조 표기. |
|
| 256 |
-.tokenFigma {
|
|
| 257 |
- display: block; |
|
| 258 |
- margin-block-start: fox.gap(1); |
|
| 259 |
- color: fox.color(font-neutral-subtler); |
|
| 260 |
- font-size: fox.font-size(body-xsm); |
|
| 261 |
- word-break: break-all; |
|
| 262 |
-} |
|
| 263 | 255 |
|
| 264 | 256 |
// 다른 토큰을 가리키는 값. 실제 값과 구분되어야 "이게 참조구나"가 한눈에 보인다. |
| 265 | 257 |
.aliasRef {
|
| 266 | 258 |
color: fox.color(font-theme-primary); |
| 267 | 259 |
} |
| 268 | 260 |
|
| 269 |
-// 제공되지 않은 컬렉션을 가리키는 참조 — 감사에서 걸러내야 할 항목이다. |
|
| 270 |
-.aliasForeign {
|
|
| 271 |
- color: fox.color(font-system-warning); |
|
| 272 |
-} |
|
| 273 | 261 |
|
| 274 | 262 |
// 서체 토큰 미리보기 문장. |
| 275 | 263 |
.demoType {
|
--- @fox/dev-test/read-css-tokens.ts
+++ @fox/dev-test/read-css-tokens.ts
... | ... | @@ -1,5 +1,3 @@ |
| 1 |
-import { TOKEN_MANIFEST } from "./token-manifest";
|
|
| 2 |
- |
|
| 3 | 1 |
export interface CssToken {
|
| 4 | 2 |
/** `--fox-` 를 뗀 이름 */ |
| 5 | 3 |
name: string; |
... | ... | @@ -8,13 +6,6 @@ |
| 8 | 6 |
declaration: string; |
| 9 | 7 |
/** 참조면 가리키는 토큰 이름, 아니면 null */ |
| 10 | 8 |
alias: string | null; |
| 11 |
- /** Figma 변수 경로 — CSS에 없는 정보라 매니페스트에서 채운다. 없으면 null. */ |
|
| 12 |
- figma: string | null; |
|
| 13 |
- /** |
|
| 14 |
- * 원본 Figma에서는 참조였지만 그 컬렉션이 제공되지 않아, 생성기가 리터럴로 떨궈 |
|
| 15 |
- * CSS에는 참조 흔적이 남지 않은 경우. 이것도 CSS가 담지 못하는 정보다. |
|
| 16 |
- */ |
|
| 17 |
- foreignAlias: string | null; |
|
| 18 | 9 |
} |
| 19 | 10 |
|
| 20 | 11 |
export interface CssSection {
|
... | ... | @@ -27,13 +18,6 @@ |
| 27 | 18 |
} |
| 28 | 19 |
|
| 29 | 20 |
const PREFIX = "--fox-"; |
| 30 |
- |
|
| 31 |
-// CSS가 담지 못하는 정보(Figma 변수 경로, "미제공 컬렉션 참조였다"는 사실)만 매니페스트에서 |
|
| 32 |
-// 가져온다. 값 자체는 절대 여기서 읽지 않는다 — 값의 출처는 컴파일된 CSS다. |
|
| 33 |
-const MANIFEST_INDEX = new Map<string, Map<string, (typeof TOKEN_MANIFEST)[number]["tokens"][number]>>(); |
|
| 34 |
-for (const section of TOKEN_MANIFEST) {
|
|
| 35 |
- MANIFEST_INDEX.set(section.id, new Map(section.tokens.map((token) => [token.name, token]))); |
|
| 36 |
-} |
|
| 37 | 21 |
|
| 38 | 22 |
/** `var(--fox-x)` 형태면 가리키는 토큰 이름을 돌려준다. */ |
| 39 | 23 |
function aliasOf(value: string): string | null {
|
... | ... | @@ -99,17 +83,12 @@ |
| 99 | 83 |
return null; |
| 100 | 84 |
} |
| 101 | 85 |
|
| 102 |
-function toToken(sectionId: string, name: string, declaration: string): CssToken {
|
|
| 103 |
- const meta = MANIFEST_INDEX.get(sectionId)?.get(name); |
|
| 104 |
- const foreign = meta?.alias?.includes("미제공") ? meta.alias : null;
|
|
| 105 |
- |
|
| 86 |
+function toToken(name: string, declaration: string): CssToken {
|
|
| 106 | 87 |
return {
|
| 107 | 88 |
name, |
| 108 | 89 |
property: `${PREFIX}${name}`,
|
| 109 | 90 |
declaration, |
| 110 | 91 |
alias: aliasOf(declaration), |
| 111 |
- figma: meta?.figma ?? null, |
|
| 112 |
- foreignAlias: foreign, |
|
| 113 | 92 |
}; |
| 114 | 93 |
} |
| 115 | 94 |
|
... | ... | @@ -169,8 +148,7 @@ |
| 169 | 148 |
* |
| 170 | 149 |
* **매니페스트가 아니라 실제 스타일시트가 값의 출처다.** 토큰 SCSS를 고치면(원칙적으로 |
| 171 | 150 |
* 자동 생성물이라 고치면 안 되지만) 이 화면에 즉시 반영되므로, 생성물과 화면이 어긋날 |
| 172 |
- * 여지가 없다. CSS가 담지 못하는 Figma 변수 경로만 매니페스트에서 이름으로 가져온다. |
|
| 173 |
- * |
|
| 151 |
+ * 여지가 없다. |
|
| 174 | 152 |
* 선언 **원문**을 읽는 것이 요점이다 — 계산값을 읽으면 참조(`var(--fox-…)`)가 이미 |
| 175 | 153 |
* 풀려 버리고, 현재 테마·뷰포트 한쪽 값만 보인다. 원문에는 light/dark와 PC/모바일이 |
| 176 | 154 |
* 모두 남아 있어 현재 화면 상태와 무관하게 네 조합을 전부 볼 수 있다. |
... | ... | @@ -220,31 +198,31 @@ |
| 220 | 198 |
label: "Light", |
| 221 | 199 |
category: "Mode", |
| 222 | 200 |
source: "컴파일된 CSS — light-dark()의 라이트 쪽", |
| 223 |
- tokens: colors.map((entry) => toToken("light", entry.name, entry.light)),
|
|
| 201 |
+ tokens: colors.map((entry) => toToken(entry.name, entry.light)), |
|
| 224 | 202 |
}, |
| 225 | 203 |
{
|
| 226 | 204 |
id: "dark", |
| 227 | 205 |
label: "Dark", |
| 228 | 206 |
category: "Mode", |
| 229 | 207 |
source: "컴파일된 CSS — light-dark()의 다크 쪽", |
| 230 |
- tokens: colors.map((entry) => toToken("dark", entry.name, entry.dark)),
|
|
| 208 |
+ tokens: colors.map((entry) => toToken(entry.name, entry.dark)), |
|
| 231 | 209 |
}, |
| 232 | 210 |
{
|
| 233 | 211 |
id: "pc", |
| 234 | 212 |
label: "PC", |
| 235 | 213 |
category: "Responsive", |
| 236 | 214 |
source: "컴파일된 CSS — @media (min-width: 768px) 블록", |
| 237 |
- tokens: responsive.map((name) => toToken("pc", name, pc.get(name) ?? "")),
|
|
| 215 |
+ tokens: responsive.map((name) => toToken(name, pc.get(name) ?? "")), |
|
| 238 | 216 |
}, |
| 239 | 217 |
{
|
| 240 | 218 |
id: "mobile", |
| 241 | 219 |
label: "Mobile", |
| 242 | 220 |
category: "Responsive", |
| 243 | 221 |
source: "컴파일된 CSS — :root 기본값(모바일 우선)", |
| 244 |
- tokens: responsive.map((name) => toToken("mobile", name, base.get(name) ?? "")),
|
|
| 222 |
+ tokens: responsive.map((name) => toToken(name, base.get(name) ?? "")), |
|
| 245 | 223 |
}, |
| 246 |
- { id: "size", label: "Size", category: null, source: "컴파일된 CSS — :root", tokens: size.map((n) => toToken("size", n, base.get(n) ?? "")) },
|
|
| 247 |
- { id: "theme", label: "Theme", category: null, source: "컴파일된 CSS — :root", tokens: theme.map((n) => toToken("theme", n, base.get(n) ?? "")) },
|
|
| 248 |
- { id: "primitive", label: "Primitive", category: null, source: "컴파일된 CSS — :root", tokens: primitive.map((n) => toToken("primitive", n, base.get(n) ?? "")) },
|
|
| 224 |
+ { id: "size", label: "Size", category: null, source: "컴파일된 CSS — :root", tokens: size.map((n) => toToken(n, base.get(n) ?? "")) },
|
|
| 225 |
+ { id: "theme", label: "Theme", category: null, source: "컴파일된 CSS — :root", tokens: theme.map((n) => toToken(n, base.get(n) ?? "")) },
|
|
| 226 |
+ { id: "primitive", label: "Primitive", category: null, source: "컴파일된 CSS — :root", tokens: primitive.map((n) => toToken(n, base.get(n) ?? "")) },
|
|
| 249 | 227 |
]; |
| 250 | 228 |
} |
--- @fox/dev-test/token-manifest.ts
| This file is too big to display. |
--- @fox/dev-test/token-view.tsx
+++ @fox/dev-test/token-view.tsx
... | ... | @@ -133,7 +133,6 @@ |
| 133 | 133 |
|
| 134 | 134 |
export function TokenView({ section }: { section: CssSection }) {
|
| 135 | 135 |
const hasPreview = section.tokens.some((token) => previewFor(token) !== null); |
| 136 |
- const foreignCount = section.tokens.filter((token) => token.foreignAlias).length; |
|
| 137 | 136 |
|
| 138 | 137 |
return ( |
| 139 | 138 |
<section className={styles.section}>
|
... | ... | @@ -141,11 +140,8 @@ |
| 141 | 140 |
{section.label} · {section.tokens.length}
|
| 142 | 141 |
</h2> |
| 143 | 142 |
<p className={styles.sectionNote}>
|
| 144 |
- {section.source} — 값은 매니페스트가 아니라 실제 스타일시트에서 읽습니다. 참조면
|
|
| 145 |
- 가리키는 토큰을, 아니면 값을 표시합니다. |
|
| 146 |
- {foreignCount > 0
|
|
| 147 |
- ? ` 이 중 ${foreignCount}개는 원본 Figma에서 제공되지 않은 컬렉션을 가리켜 값으로 굳어졌습니다.`
|
|
| 148 |
- : ""} |
|
| 143 |
+ {section.source} — 값은 컴파일된 스타일시트에서 직접 읽습니다. 참조면 가리키는 토큰을,
|
|
| 144 |
+ 아니면 값을 표시합니다. |
|
| 149 | 145 |
</p> |
| 150 | 146 |
|
| 151 | 147 |
<table className={styles.table}>
|
... | ... | @@ -161,7 +157,6 @@ |
| 161 | 157 |
<tr key={token.name}>
|
| 162 | 158 |
<td> |
| 163 | 159 |
<span className={styles.tokenName}>{token.name}</span>
|
| 164 |
- <span className={styles.tokenFigma}>{token.figma}</span>
|
|
| 165 | 160 |
</td> |
| 166 | 161 |
<td className={styles.tdValue}>
|
| 167 | 162 |
{isColor(token) ? (
|
... | ... | @@ -174,15 +169,6 @@ |
| 174 | 169 |
) : ( |
| 175 | 170 |
token.declaration |
| 176 | 171 |
)} |
| 177 |
- {token.foreignAlias ? (
|
|
| 178 |
- <span |
|
| 179 |
- className={styles.aliasForeign}
|
|
| 180 |
- title="원본 Figma에서는 참조였으나 해당 컬렉션이 제공되지 않아 값으로 굳어졌습니다" |
|
| 181 |
- > |
|
| 182 |
- {" "}
|
|
| 183 |
- ({token.foreignAlias})
|
|
| 184 |
- </span> |
|
| 185 |
- ) : null} |
|
| 186 | 172 |
</td> |
| 187 | 173 |
{hasPreview ? <td className={styles.tdDemo}>{previewFor(token)}</td> : null}
|
| 188 | 174 |
</tr> |
--- @fox/tools/build-tokens.py
+++ @fox/tools/build-tokens.py
... | ... | @@ -48,13 +48,6 @@ |
| 48 | 48 |
raw[key] = OrderedDict(leaves(doc)) |
| 49 | 49 |
|
| 50 | 50 |
|
| 51 |
-# 참조 판별용 원본 값 — resolve_refs가 덮어쓰기 전에 보관한다. 감사 화면이 |
|
| 52 |
-# "파일에 실제 값이 적혀 있었는가, 다른 토큰을 가리켰는가"를 구분하려면 원본이 필요하다. |
|
| 53 |
-ORIGINAL_VALUE = {
|
|
| 54 |
- (coll, path): node["$value"] for coll, table in raw.items() for path, node in table.items() |
|
| 55 |
-} |
|
| 56 |
- |
|
| 57 |
- |
|
| 58 | 51 |
def resolve_refs(table): |
| 59 | 52 |
"""같은 파일 안의 {a.b.c} 참조를 실제 값으로 치환한다."""
|
| 60 | 53 |
for path, node in table.items(): |
... | ... | @@ -479,139 +472,6 @@ |
| 479 | 472 |
|
| 480 | 473 |
@use "root"; |
| 481 | 474 |
""")) |
| 482 |
- |
|
| 483 |
-# ── 감사용 매니페스트 ──────────────────────────────────────────────────────── |
|
| 484 |
-# Figma 변수 페이지와 1:1로 대조하기 위한 데이터다. SCSS/CSS는 alias를 var() 참조로 |
|
| 485 |
-# 흡수하고 light/dark를 light-dark() 한 줄로 합치기 때문에, 브라우저에서 읽으면 |
|
| 486 |
-# "원래 파일이 무엇을 적어 뒀는지"를 되살릴 수 없다. 그래서 원본 구조 그대로를 따로 낸다. |
|
| 487 |
-NAMERS = {
|
|
| 488 |
- "primitive": name_primitive, |
|
| 489 |
- "theme": name_theme, |
|
| 490 |
- "size": name_size, |
|
| 491 |
- "light": name_semantic, |
|
| 492 |
- "dark": name_semantic, |
|
| 493 |
- "pc": name_responsive, |
|
| 494 |
- "mobile": name_responsive, |
|
| 495 |
-} |
|
| 496 |
- |
|
| 497 |
-SECTIONS = [ |
|
| 498 |
- ("light", "Light", "Mode"),
|
|
| 499 |
- ("dark", "Dark", "Mode"),
|
|
| 500 |
- ("pc", "PC", "Responsive"),
|
|
| 501 |
- ("mobile", "Mobile", "Responsive"),
|
|
| 502 |
- ("size", "Size", None),
|
|
| 503 |
- ("theme", "Theme", None),
|
|
| 504 |
- ("primitive", "Primitive", None),
|
|
| 505 |
-] |
|
| 506 |
- |
|
| 507 |
- |
|
| 508 |
-def literal_text(coll, path, node): |
|
| 509 |
- """파일에 적힌 값을 Figma 표기 그대로 낸다 — 변수 페이지와 눈으로 맞추기 위함이다.""" |
|
| 510 |
- value = node["$value"] |
|
| 511 |
- if node["$type"] == "color": |
|
| 512 |
- return css_color(value) |
|
| 513 |
- if node["$type"] == "string": |
|
| 514 |
- return str(value) |
|
| 515 |
- return str(value) |
|
| 516 |
- |
|
| 517 |
- |
|
| 518 |
-def resolved_text(coll, path, node): |
|
| 519 |
- """우리 단위로 변환된 최종 값. 길이는 rem, 색은 hex/rgb().""" |
|
| 520 |
- value = node["$value"] |
|
| 521 |
- if node["$type"] == "color": |
|
| 522 |
- return css_color(value) |
|
| 523 |
- if node["$type"] == "string": |
|
| 524 |
- return str(value) |
|
| 525 |
- if coll == "primitive" and path.startswith("font.weight"):
|
|
| 526 |
- return str(value) |
|
| 527 |
- return css_len(value) |
|
| 528 |
- |
|
| 529 |
- |
|
| 530 |
-def manifest_rows(coll): |
|
| 531 |
- namer = NAMERS[coll] |
|
| 532 |
- rows = [] |
|
| 533 |
- # light/dark는 SCSS 출력과 같은 순서로 맞춘다 — 소속이 불분명한 chart-alt 29개를 |
|
| 534 |
- # 뒤로 보내야 주요 색상이 먼저 보인다(원본 JSON에서는 이들이 루트에 먼저 온다). |
|
| 535 |
- entries = raw[coll].items() |
|
| 536 |
- if coll in ("light", "dark"):
|
|
| 537 |
- entries = sorted(entries, key=lambda kv: kv[0] in CHART_ALT_GROUP) |
|
| 538 |
- for path, node in entries: |
|
| 539 |
- original = ORIGINAL_VALUE[(coll, path)] |
|
| 540 |
- alias = None |
|
| 541 |
- literal = None |
|
| 542 |
- |
|
| 543 |
- if isinstance(original, str) and original.startswith("{"):
|
|
| 544 |
- # 같은 파일 안의 다른 토큰을 가리킨다. |
|
| 545 |
- alias = namer(original[1:-1]) |
|
| 546 |
- else: |
|
| 547 |
- data = node.get("$extensions", {}).get("com.figma.aliasData")
|
|
| 548 |
- if data: |
|
| 549 |
- hit = alias_lookup.get((data["targetVariableSetName"], data["targetVariableName"])) |
|
| 550 |
- # ⚠️ 이름만으로 매칭하면 안 된다 — 이 export에는 `theme`이라는 이름의 |
|
| 551 |
- # 컬렉션이 둘 있고(제공된 파랑 계열, 미제공 청록 계열) 변수 경로까지 |
|
| 552 |
- # 같다. 값이 일치할 때만 우리 토큰으로 인정하고, 아니면 우리가 갖고 |
|
| 553 |
- # 있지 않은 컬렉션을 가리키는 것으로 표시한다. |
|
| 554 |
- if hit and same_value(hit[1], node["$value"]): |
|
| 555 |
- alias = hit[0] |
|
| 556 |
- else: |
|
| 557 |
- alias = f'{data["targetVariableSetName"]}/{data["targetVariableName"]} (미제공 컬렉션)'
|
|
| 558 |
- else: |
|
| 559 |
- literal = literal_text(coll, path, node) |
|
| 560 |
- |
|
| 561 |
- rows.append( |
|
| 562 |
- {
|
|
| 563 |
- "name": namer(path), |
|
| 564 |
- "figma": path.replace(".", "/"),
|
|
| 565 |
- "type": node["$type"], |
|
| 566 |
- "literal": literal, |
|
| 567 |
- "alias": alias, |
|
| 568 |
- # 그 파일/모드에서 최종 해석된 값(우리 단위 기준). 미리보기가 현재 |
|
| 569 |
- # 테마·뷰포트가 아니라 **그 섹션이 정의한 값**을 그려야 하므로, CSS |
|
| 570 |
- # 변수가 아니라 이 값을 직접 쓴다. |
|
| 571 |
- "resolved": resolved_text(coll, path, node), |
|
| 572 |
- } |
|
| 573 |
- ) |
|
| 574 |
- return rows |
|
| 575 |
- |
|
| 576 |
- |
|
| 577 |
-manifest = [ |
|
| 578 |
- {"id": coll, "label": label_, "category": category, "file": FILES[coll], "tokens": manifest_rows(coll)}
|
|
| 579 |
- for coll, label_, category in SECTIONS |
|
| 580 |
-] |
|
| 581 |
- |
|
| 582 |
-manifest_ts = """// ⚠️ 이 파일은 Figma Design Tokens export에서 **자동 생성**되었습니다. |
|
| 583 |
-// 손으로 고치지 말고 원본 JSON을 다시 export한 뒤 `@fox/tools/build-tokens.py`를 재실행하세요. |
|
| 584 |
-// |
|
| 585 |
-// Figma 변수 페이지와 대조하기 위한 데이터입니다. CSS는 alias를 var() 참조로 흡수하고 |
|
| 586 |
-// light/dark를 light-dark() 한 줄로 합치므로, 브라우저에서 읽어서는 "원본 파일이 무엇을 |
|
| 587 |
-// 적어 뒀는지"를 되살릴 수 없습니다 — 그래서 원본 구조를 그대로 담습니다. |
|
| 588 |
- |
|
| 589 |
-export interface ManifestToken {
|
|
| 590 |
- /** 우리 CSS 변수 이름에서 `--fox-`를 뗀 것 */ |
|
| 591 |
- name: string; |
|
| 592 |
- /** 원본 JSON에서의 경로 (= Figma 변수 이름) */ |
|
| 593 |
- figma: string; |
|
| 594 |
- type: "color" | "number" | "string"; |
|
| 595 |
- /** 파일에 값이 직접 적혀 있을 때만 채워진다 */ |
|
| 596 |
- literal: string | null; |
|
| 597 |
- /** 다른 토큰을 가리킬 때만 채워진다 */ |
|
| 598 |
- alias: string | null; |
|
| 599 |
- /** 그 파일/모드에서 최종 해석된 값 — 미리보기 전용(현재 테마·뷰포트와 무관) */ |
|
| 600 |
- resolved: string; |
|
| 601 |
-} |
|
| 602 |
- |
|
| 603 |
-export interface ManifestSection {
|
|
| 604 |
- id: string; |
|
| 605 |
- label: string; |
|
| 606 |
- /** 사이드바 상위 분류. 없으면 최상위 항목이다. */ |
|
| 607 |
- category: string | null; |
|
| 608 |
- file: string; |
|
| 609 |
- tokens: ManifestToken[]; |
|
| 610 |
-} |
|
| 611 |
- |
|
| 612 |
-export const TOKEN_MANIFEST: ManifestSection[] = """ + json.dumps(manifest, ensure_ascii=False, indent=2) + ";\n" |
|
| 613 |
- |
|
| 614 |
-written.append(write("../dev-test/token-manifest.ts", manifest_ts))
|
|
| 615 | 475 |
|
| 616 | 476 |
print("생성된 파일:")
|
| 617 | 477 |
for w in written: |
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?