tokens · diff

git:20260826.dfbfa9d to git:20260826.bd10424

81 added, 81 removed. Audit A to A.

---
name: tokens
description: Audits whether the colors on a Figma frame or page are bound to design system tokens, and maps and binds hardcoded colors to the right token. Derives token-to-hex from bindings already in the file so every mapping has evidence, grades the results, and auto-proposes only the safe ones. Lint mode (check only) and bind mode are separate. Which library and which token groups come from the design_system section of figma-conventions.yaml; on auto it detects the library the file is connected to. Triggers - "/fig:tokens", "check the token bindings", "find unbound colors", "토큰 바인딩 검수", "하드코딩 색상 검사", "디자인시스템 연동 점검".
allowed-tools: AskUserQuestion, mcp__plugin_figma_figma__use_figma, mcp__plugin_figma_figma__search_design_system, mcp__plugin_figma_figma__get_screenshot, mcp__plugin_figma_figma__get_metadata
---
- # fig:tokens — 디자인시스템 토큰 바인딩 검수·정비
+ # fig:tokens — audit and repair design system token bindings
- 프레임 안의 색이 "디자인시스템 변수에 묶여 있는가"를 검수하고, 묶이지 않은 하드코딩 색을 **근거 있는 매핑**으로 토큰에 연결한다. 색 하나하나가 토큰을 타야 테마·리브랜딩·일관성이 유지되므로, 핸드오프 전 마지막 점검 단계로 쓴다.
+ Checks whether the colors inside a frame are actually bound to design system variables, and connects unbound hardcoded colors to a token through **a mapping that has evidence behind it**. Theming, rebranding, and consistency only hold if every color goes through a token, so this runs as the last check before handoff.
- **전제**: `use_figma` 호출 전 반드시 `figma:figma-use` 스킬을 먼저 로드한다.
+ **Prerequisites**: always load the `figma:figma-use` skill before calling `use_figma`.
## When to invoke
- - "이 프레임 색이 토큰에 다 묶였는지 봐줘", "변수 안 묶인 색 찾아줘"
- - "하드코딩 hex를 토큰으로 일괄 바꿔줘"
- - 디자인 완성 후 핸드오프 직전 토큰 연동 점검
+ - "check whether this frame's colors are all bound", "find colors that aren't on a variable"
+ - "convert the hardcoded hex values to tokens"
+ - The token check right before handoff, once the design is done
## When NOT to invoke
- - 프레임 네이밍·섹션 정리 → `/fig:prep`
- - 구조·흐름 규칙 위반 검증 → `/fig:lint`
- - 흐름 화살표 → `/fig:arrows`
- - 토큰(변수) 자체를 새로 만들기 → `figma:figma-generate-library`
- - 화면 디자인 생성 → `figma:figma-generate-design`
+ - Frame naming and section tidying → `/fig:prep`
+ - Structure and flow rule violations → `/fig:lint`
+ - Flow arrows → `/fig:arrows`
+ - Creating the tokens (variables) themselves → `figma:figma-generate-library`
+ - Designing screens → `figma:figma-generate-design`
## Inputs
- - `figma_url` (필수): 검수할 페이지·프레임 URL
- - `mode` (선택): Lint 모드(검사만) / 바인딩 실행. 생략하면 검사 후 매핑표로 제안한다
+ - `figma_url` (required): the page or frame to audit
+ - `mode` (optional): lint mode (check only) / bind. Omitted, it checks and proposes a mapping table
- 규칙 원천은 `figma-conventions.yaml` 이다 — `resolve-config.py --js <fileKey>` 로 `design_system` 절(라이브러리·토큰군·임계)을 읽는다.
+ The rules come from `figma-conventions.yaml` — read the `design_system` section (library, token groups, thresholds) with `resolve-config.py --js <fileKey>`.
- ## 왜 이 스킬이 따로 필요한가 — 도구 제약
+ ## Why this needs its own skill — tool constraints
- - **`get_metadata`로는 색을 못 본다.** 메타데이터에는 노드 ID·타입·좌표·크기만 있고 fill/stroke/effect 색 정보가 없다. 게다가 큰 프레임은 메타데이터가 토큰 한도를 넘겨 통째로 실패한다. 따라서 **색 검사·바인딩은 전부 `use_figma` 스크립트로 노드를 직접 순회**해서 한다.
- - **`search_design_system`은 토큰의 hex 값을 주지 않는다.** 토큰의 이름·키·scope·소속 라이브러리만 준다. 실제 색값은 따로 알아내야 한다(아래 "토큰 hex 역추출").
+ - **`get_metadata` cannot see colors.** Metadata carries node id, type, coordinates, and size, with no fill, stroke, or effect color. On a large frame it also blows the token limit and fails wholesale. So **all color inspection and binding walks nodes directly through a `use_figma` script.**
+ - **`search_design_system` does not return a token's hex.** It returns the name, key, scope, and owning library. The actual color has to be worked out separately (see below).
- ## 토큰 출처 — 자동 감지 + hex 역추출 (핵심)
+ ## Where token values come from — auto-detection plus reverse-derivation (the core)
- 매핑의 신뢰도는 "이 토큰이 실제로 무슨 색인가"를 정확히 아는 데서 나온다. 두 경로를 병행한다.
+ A mapping is only as trustworthy as your knowledge of "what color is this token, really". Two routes run together.
- 1. **연결 라이브러리 감지**: `search_design_system` 으로 설정 `design_system.token_prefixes` 의 토큰군을 조회해 이름·키를 확보한다. 대상 라이브러리는 `design_system.library` 가 정하고, `auto` 면 파일에 연결된 라이브러리를 감지한다. **라이브러리 이름을 본문에 박지 않는다** — 파일마다 다르다. `token_prefixes` 가 비어 있으면 역추출 맵(2번)에 등장한 접두사를 그대로 후보로 쓴다.
- 2. **기존 바인딩에서 토큰→hex 역추출 (가장 신뢰도 높음)**: 같은 파일에서 *이미 변수에 바인딩된* 색들을 순회해 `{토큰명 → 실제 hex}` 맵을 만든다. 바인딩된 paint는 `boundVariables.color`로 토큰 ID를, `paint.color`로 마지막 해석된 실제 색을 함께 들고 있다. 이 파일이 실제로 그 토큰을 어떤 색으로 쓰는지를 그대로 보여주므로 매핑의 1차 근거가 된다.
+ 1. **Detect the connected library**: query the token groups in `design_system.token_prefixes` with `search_design_system` to get names and keys. `design_system.library` decides the target, and on `auto` it detects the library the file is connected to. **Never hardcode a library name in this document** — it differs per file. When `token_prefixes` is empty, use whatever prefixes appear in the reverse-derived map (route 2) as candidates.
+ 2. **Reverse-derive token-to-hex from existing bindings (the most reliable)**: walk the colors in the same file that are *already bound to a variable* and build a `{token name → actual hex}` map. A bound paint carries both the token id in `boundVariables.color` and the last resolved color in `paint.color`. That shows exactly what color this file actually renders that token as, which makes it the primary evidence for any mapping.
- > **주의 — 토큰 표면값은 요청 hex와 다를 수 있다.** 예를 들어 "어떤 회색을 `fill/primary`로 바꿔줘"라고 해도 `fill/primary`의 실제값은 그 회색과 미세하게 다를 수 있다(토큰화가 목적이면 정상). 색이 바뀌는 경우 **반드시 미리보기에 "이 토큰은 #AAA → 화면이 #BBB로 바뀜"을 명시**한다.
+ > **Caution — a token's real value may differ from the hex you were given.** "Change this grey to `fill/primary`" can still mean `fill/primary` is slightly different from that grey (which is fine if tokenizing is the goal). Whenever the color changes, **the preview must say "this token is #AAA, so the screen becomes #BBB".**
- ## 검사 대상 속성
+ ## Properties checked
- fill · stroke · 텍스트 색(TEXT 노드의 fills) · 효과 색(effects의 color)을 모두 검사한다. 속성별로 매칭 가능한 토큰 scope가 다르다(예: 배경 fill ↔ `FRAME_FILL/SHAPE_FILL`, 테두리 ↔ `STROKE`, 텍스트 ↔ `TEXT_FILL`). **속성과 scope가 맞는 토큰끼리만 매핑한다** — 텍스트색을 테두리 토큰에 묶지 않는다.
+ fill · stroke · text color (a TEXT node's fills) · effect color (an effect's color) are all checked. Different properties match different token scopes — a background fill against `FRAME_FILL`/`SHAPE_FILL`, a border against `STROKE`, text against `TEXT_FILL`. **Only map a property to a token whose scope fits it** — never bind a text color to a border token.
- ## 제외 대상 (검수·바인딩 모두에서 건드리지 않음)
+ ## Excluded (untouched in both audit and binding)
- | 대상 | 이유 |
+ | Target | Why |
|---|---|
- | 컴포넌트 **인스턴스 내부** 색 | 인스턴스 오버라이드라 깨지기 쉽고, 원본 컴포넌트(라이브러리) 쪽에서 토큰화해야 할 별도 이슈. 인스턴스 노드 *자신*의 색(오버라이드)은 대상이나, 그 *내부 자식*은 제외 |
- | 작업용 스캐폴딩 | 설정 `naming` 의 스캐폴딩 이름 규약(`arrow_delimiter`·`label_prefix`·`state_chain_prefix`)에 걸리는 노드와 `section_style`·`placeholder_style` 로 칠해진 것 — `/fig:arrows`·`/fig:prep` 산출물이라 토큰화 대상이 아니다 |
- | 사용자가 지정한 보호 영역 | 아카이브·템플릿 등 |
+ | Colors **inside a component instance** | They are instance overrides, fragile, and really a separate issue to be tokenized on the source component in the library. The instance node's *own* color (an override) is in scope; its *children* are not |
+ | Working scaffolding | Nodes matching the scaffolding name conventions in `naming` (`arrow_delimiter`, `label_prefix`, `state_chain_prefix`) and anything painted with `section_style` or `placeholder_style` — those are `/fig:arrows` and `/fig:prep` output, not design to tokenize |
+ | User-designated protected areas | Archives, templates, and the like |
- 발견한 제외 항목은 **고치지 말고 보고만** 한다(특히 인스턴스 내부 색은 "컴포넌트 정비 이슈"로 따로 묶어 전달).
+ Excluded items that turn up are **reported, never fixed** — colors inside instances in particular get grouped separately as "a component repair issue".
- ## 등급 분류 — 안전한 것만 자동 제안
+ ## Grading — only auto-propose what is safe
- 미바인딩 색을, 같은 속성의 가장 가까운 토큰과 대조해 3등급으로 나눈다.
+ Each unbound color is compared against the nearest token of the same property and graded.
- | 등급 | 기준 | 처리 |
+ | Grade | Basis | Handling |
|---|---|---|
- | 1등급 — 정확/근접 일치 | 토큰 hex 와 동일하거나 채널 차가 설정 `design_system.match_threshold_channel` 이하 | 바인딩해도 색이 사실상 안 변함 → **자동 매핑 제안** |
- | 2등급 — 색 변동 | 가장 가까운 토큰과 눈에 띄게 차이남(팔레트 밖 색) | 스냅하면 색이 바뀜 → **보고만, 진행은 사용자 판단** |
- | 3등급 — 대응 토큰 없음 | 어느 토큰과도 멀거나 브랜드/특수색 | 바인딩 비권장, 보고만 |
+ | 1 — exact or near match | Identical to the token's hex, or within `design_system.match_threshold_channel` per channel | Binding effectively does not change the color → **auto-propose the mapping** |
+ | 2 — color shifts | Visibly different from the nearest token (off-palette) | Snapping changes the color → **report only; the user decides** |
+ | 3 — no matching token | Far from every token, or a brand or special color | Binding not advised, report only |
- 임계값은 기준선일 뿐이다 — 경계의 색은 "Δ 얼마"를 같이 보여줘 사용자가 판단하게 한다.
+ The threshold is a baseline, not a verdict — for borderline colors, show the delta so the user can judge.
- ## Lint 모드 — 검사만 (쓰기 없음)
+ ## Lint mode — check only (no writes)
- "검사만/현황만" 요청이거나 정비 전 파악 단계. 읽기 전용으로 리포트만 낸다:
+ For "just check" requests, or the survey step before repair. Read-only, report only:
- - 대상 범위 안 미바인딩 색을 **hex별로 집계**(속성·곳 수·대표 위치), 인스턴스 내부/스캐폴딩/직접수정가능으로 분류
- - 직접 수정 가능한 색은 등급 분류표 + 추천 토큰·Δ를 붙인다
- - 수정은 하지 않고, 별도 go를 받아 아래 Procedure로 진행
+ - Unbound colors in scope **aggregated by hex** (property, count, representative locations), classified as inside-instance / scaffolding / directly editable
+ - Directly editable colors get the grade table plus a recommended token and delta
+ - Nothing is changed; proceeding requires a separate go into the procedure below
## Procedure
- ### 1. 토큰 사전 구축
+ ### 1. Build the token dictionary
- - `search_design_system` 으로 설정 `design_system` 의 토큰군 이름·키 확보
- - 대상 프레임을 순회해 **기존 바인딩 색 → {토큰명, 실제 hex}** 역추출 맵 생성
- - 두 결과를 합쳐 `{토큰명, 키, scope, hex}` 사전을 만든다 — 매핑의 근거 테이블
+ - Get names and keys for the configured token groups via `search_design_system`
+ - Walk the target frames and build the **existing binding → {token name, actual hex}** reverse map
+ - Merge both into a `{token name, key, scope, hex}` dictionary — the evidence table behind every mapping
- ### 2. 미바인딩 색 스캔 (읽기 전용)
+ ### 2. Scan for unbound colors (read-only)
- - 대상 프레임을 순회하며 fill·stroke·텍스트·효과의 **SOLID이고 미바인딩**인 색을 hex별 집계
- - 각 노드를 인스턴스 내부 / 스캐폴딩 / 직접수정가능으로 분류(아래 스니펫)
- - 직접수정가능 색에 대해 사전과 대조해 등급·추천토큰·Δ 산출 → 매핑표
+ - Walk the target frames and aggregate **SOLID and unbound** colors from fills, strokes, text, and effects, by hex
+ - Classify each node as inside-instance / scaffolding / directly editable (see the snippet below)
+ - For the directly editable ones, compare against the dictionary to derive grade, recommended token, and delta → the mapping table
- ### 3. 매핑 확정 (사용자 선택)
+ ### 3. Settle the mapping (user's choice)
- - 1등급을 표로 제시. **하나의 hex가 의미가 다른 두 토큰에 동일 매칭되면**(예: 흰색이 `Static/white`와 `Background/normal/normal` 둘 다) 임의로 고르지 말고 의미 차이를 붙여 선택하게 한다
- - 2·3등급은 보고만 하고, 진행 여부는 사용자에게
- - 색이 바뀌는 매핑(2등급 등)은 변동 사실을 명시
+ - Present grade 1 as a table. **When one hex matches two tokens with different meanings equally** (white matching both a static white and a background token, say), do not pick — present the difference in meaning and let the user choose
+ - Grades 2 and 3 are reported only; the user decides whether to proceed
+ - Any mapping that changes the color says so explicitly
- ### 4. 바인딩 (미리보기 → go, 단계 분할)
+ ### 4. Bind (preview → go, split into steps)
- - 확정 매핑을 **미리보기**(대상·hex→토큰·곳 수·제외 사항)로 보여주고 **go**를 받는다
- - 양이 많으면 **프레임별 또는 색상별로 분할** 실행 — 한 번에 다 쓰지 않는다
- - `importVariableByKeyAsync`로 토큰을 임포트하고, hex가 정확히 일치하는 미바인딩 paint만 바인딩
+ - Show the settled mapping as a **preview** (targets, hex → token, counts, exclusions) and get a **go**
+ - With a lot to do, **split by frame or by color** — never write it all at once
+ - Import tokens with `importVariableByKeyAsync`, and bind only unbound paints whose hex matches exactly
- ### 5. 검증 (필수)
+ ### 5. Verify (required)
- - **바인딩 후 재스캔**해 "직접 수정 가능한 대상 색의 미바인딩 잔여 = 0"을 확인한다
- - 집계 단계의 곳 수와 실제 바인딩 수가 다를 수 있다 — 인스턴스 내부 노드 탐색 범위가 호출 간 흔들려 분류가 달라질 수 있기 때문(추정). **누락이 아님은 재스캔의 잔여 0으로 입증**하고, 차이는 정직하게 보고한다
- - 남은 인스턴스 내부 색·2/3등급은 "이번 범위 밖"으로 분리해 보고
+ - **Re-scan after binding** and confirm "unbound remainder among the targeted, directly editable colors = 0"
+ - The count from the aggregation step and the number actually bound may differ — the traversal boundary for nodes inside instances can shift between calls, changing the classification (inferred). **Prove nothing was missed by the re-scan reaching zero**, and report the discrepancy honestly
+ - Report remaining inside-instance colors and grades 2–3 separately, as out of scope for this pass
- ## 구현 스니펫
+ ## Implementation snippets
- 색→hex, 인스턴스 내부 판정:
+ Color to hex, and the inside-instance test:
```js
function hex(c){const f=x=>Math.round(x*255).toString(16).padStart(2,"0").toUpperCase();return "#"+f(c.r)+f(c.g)+f(c.b);}
function insideInstance(n){let p=n.parent;while(p){if(p.type==="INSTANCE")return true;p=p.parent;}return false;}
```
- 토큰→hex 역추출 (기존 바인딩 사용례에서):
+ Reverse-deriving token to hex from existing bindings:
```js
const map={}; // varId -> {name, hexes:{}, count}
for(const id of ROOT_IDS){
const root=await figma.getNodeByIdAsync(id);
for(const n of [root,...root.findAll(()=>true)]){
for(const key of ["fills","strokes"]){
const arr=n[key]; if(!Array.isArray(arr)) continue;
for(const p of arr){
if(p.type==="SOLID"&&p.visible!==false&&p.boundVariables&&p.boundVariables.color){
const vid=p.boundVariables.color.id;
if(!map[vid]){const v=await figma.variables.getVariableByIdAsync(vid);map[vid]={name:v?v.name:"?",hexes:{},count:0};}
map[vid].count++; const h=hex(p.color); map[vid].hexes[h]=(map[vid].hexes[h]||0)+1;
}
}
}
}
}
return Object.values(map).map(e=>({name:e.name,count:e.count,hex:Object.keys(e.hexes).join(",")}));
```
- 미바인딩 색 집계 (직접수정가능/인스턴스내부 분류):
+ Aggregating unbound colors (editable versus inside-instance):
```js
const agg={};
for(const id of ROOT_IDS){
const root=await figma.getNodeByIdAsync(id);
for(const n of [root,...root.findAll(()=>true)]){
const ii=insideInstance(n);
for(const key of ["fills","strokes"]){
const arr=n[key]; if(!Array.isArray(arr)) continue;
for(const p of arr){
if(p.type!=="SOLID"||p.visible===false) continue;
if(p.boundVariables&&p.boundVariables.color) continue;
const k=key+"|"+hex(p.color);
if(!agg[k]) agg[k]={prop:key,hex:hex(p.color),editable:0,inInstance:0,examples:[]};
agg[k][ii?"inInstance":"editable"]++;
if(agg[k].examples.length<4) agg[k].examples.push((ii?"⟨inst⟩":"")+n.name+" ["+n.type+"]");
}
}
}
}
return Object.values(agg).sort((a,b)=>b.editable-a.editable);
```
- > 텍스트 색은 위 `fills`에 TEXT 노드로 포함된다. 효과 색은 `n.effects`의 `color`를 같은 방식으로 추가 검사하고, 바인딩은 효과 전용 API를 쓴다(d.ts에서 `setBoundVariableForEffect` 계열 확인).
+ > Text colors arrive in `fills` above, on TEXT nodes. Effect colors are checked the same way through `n.effects[].color`, and bound through the effect-specific API (check the `setBoundVariableForEffect` family in the d.ts).
- 바인딩 (hex 정확 일치만, 새 paint 재할당):
+ Binding (exact hex match only, reassigning a new paint array):
```js
const V={ tokenA: await figma.variables.importVariableByKeyAsync("KEY_A"), /* ... */ };
const fillMap={"#FFFFFF":V.tokenA, /* hex: variable */ };
const strokeMap={/* ... */};
const counts={};
const root=await figma.getNodeByIdAsync(FRAME_ID);
for(const n of [root,...root.findAll(()=>true)]){
- if(insideInstance(n)) continue; // 인스턴스 내부 제외
+ if(insideInstance(n)) continue; // skip inside instances
for(const [key,mapObj] of [["fills",fillMap],["strokes",strokeMap]]){
const arr=n[key]; if(!Array.isArray(arr)||!arr.length) continue;
let changed=false;
const next=arr.map(p=>{
if(p.type==="SOLID"&&p.visible!==false&&!(p.boundVariables&&p.boundVariables.color)){
const v=mapObj[hex(p.color)];
if(v){changed=true;counts[v.name]=(counts[v.name]||0)+1;return figma.variables.setBoundVariableForPaint(p,"color",v);}
}
- return p; // fills는 읽기전용 — map으로 새 배열 생성
+ return p; // fills is read-only — build a new array with map
});
if(changed) n[key]=next;
}
}
return {frame:FRAME_ID, counts};
```
- 검증 (잔여 0 확인):
+ Verification (confirm zero remaining):
```js
let editableLeft=0;
for(const id of ROOT_IDS){
const root=await figma.getNodeByIdAsync(id);
for(const n of [root,...root.findAll(()=>true)]){
if(insideInstance(n)) continue;
for(const key of ["fills","strokes"]){
const arr=n[key]; if(!Array.isArray(arr)) continue;
for(const p of arr){
if(p.type==="SOLID"&&p.visible!==false&&!(p.boundVariables&&p.boundVariables.color)&&TARGET_HEXES.includes(hex(p.color))) editableLeft++;
}
}
}
}
- return {editableLeft}; // 0이어야 완료
+ return {editableLeft}; // must be 0 to be done
```
## Constraints
- - **쓰기 전 미리보기 → go.** 매핑표 확정과 바인딩 실행은 분리, 바인딩은 프레임별/색상별 단계 분할
- - **인스턴스 내부 색·스캐폴딩은 건드리지 않는다** — 보고만
- - 속성과 토큰 scope가 맞을 때만 매핑(텍스트색↔텍스트토큰 등)
- - 색이 바뀌는 매핑은 변동을 미리보기에 명시 — 토큰명만 보고 색이 같다고 단정하지 않는다
- - 한 호출당 작업량을 작게, 단계마다 재스캔/스크린샷으로 검증
- - 검증되지 않은 "완료"를 쓰지 않는다 — 잔여 0을 재스캔으로 입증
+ - **Preview → go before writing.** Settling the mapping and running the binding are separate; binding is split by frame or by color
+ - **Never touch colors inside instances or scaffolding** — report only
+ - Only map where the property and the token scope agree (text color to a text token, and so on)
+ - Any mapping that changes the color says so in the preview — never assume the color is unchanged from the token's name
+ - Keep each call small, verifying between steps with a re-scan or a screenshot
+ - Never write "done" without verification — prove zero remaining with a re-scan
## Notes
- - 매핑의 1차 근거는 항상 **이 파일이 실제로 쓰는 토큰값**(역추출 맵)이다 — 토큰 이름의 어감으로 추측하지 않는다
- - "미바인딩 색이 많다"가 곧 "누가 깜빡함"은 아니다 — 팔레트 밖 색이 섞여 들어온 상태일 수 있으니 2·3등급은 디자인 판단으로 넘긴다
- - 인스턴스 내부의 대량 하드코딩 색은 이 스킬의 범위가 아니라 **컴포넌트(라이브러리) 정비 과제**로 따로 제기한다
+ - The primary evidence for a mapping is always **what value this file actually renders that token as** (the reverse-derived map). Never infer from how a token's name sounds
+ - "Lots of unbound colors" does not automatically mean somebody forgot — off-palette colors may simply have found their way in, so grades 2 and 3 go back to design judgement
+ - A mass of hardcoded colors inside instances is not this skill's scope; raise it separately as **a component library repair task**