Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
afa1c46
chore(figma-token-review-plugin): scaffold plugin with selection + sc…
noahchoii Jul 3, 2026
77e9bc3
feat(figma-token-review-plugin): initial UI — tabs, focus, redesign
noahchoii Jul 3, 2026
8520483
refactor(figma-token-review-plugin): UI cleanup and restructure
noahchoii Jul 3, 2026
fe4a82b
feat(figma-token-review-plugin): wire LiteLLM + align Violation/Summa…
noahchoii Jul 3, 2026
230b289
refactor(figma-token-review-plugin): skip 🟨/🔶 prefix nodes + naming c…
noahchoii Jul 3, 2026
9082df0
refactor(figma-token-review-plugin): reorganize UI into feature folde…
noahchoii Jul 3, 2026
273f58f
feat(figma-token-review-plugin): deterministic/LLM split foundation +…
noahchoii Jul 3, 2026
0ef7037
feat(figma-token-review-plugin): per-category deterministic evaluator…
noahchoii Jul 3, 2026
b650273
feat(figma-token-review-plugin): rewire LLM for semantic-only judgmen…
noahchoii Jul 3, 2026
b705240
fix(figma-token-review-plugin): smoke findings fix waves
noahchoii Jul 3, 2026
4463849
feat(figma-token-review-plugin): rescan button + stage-3 vision conte…
noahchoii Jul 3, 2026
4eae349
feat(figma-token-review-plugin): vision context — frame screenshot + …
noahchoii Jul 3, 2026
adead8f
feat(figma-token-review-plugin): typography axis judgment + rubric ex…
noahchoii Jul 3, 2026
5cd0c8e
feat(figma-token-review-plugin): LLM PASS judgments + text-style schema
noahchoii Jul 3, 2026
9bab073
remove docs
noahchoii Jul 3, 2026
4a1488a
cleanup
noahchoii Jul 3, 2026
8d7704a
format
noahchoii Jul 7, 2026
2337136
update dependencies
noahchoii Jul 7, 2026
c90ee7a
update lockfile
noahchoii Jul 7, 2026
7423fc9
lint
noahchoii Jul 7, 2026
b2e2576
downgrade vitest version
noahchoii Jul 7, 2026
961ed27
code review
noahchoii Jul 7, 2026
f2ed7a1
update plugin id
noahchoii Jul 7, 2026
3908d77
api-key 직접 설정
noahchoii Jul 7, 2026
1d2d579
edit setting button
noahchoii Jul 7, 2026
42c3249
edit layout on setting page
noahchoii Jul 7, 2026
61b8780
remove popup size sync
noahchoii Jul 7, 2026
1fd7edd
required onClose
noahchoii Jul 8, 2026
34f81f7
token update
noahchoii Jul 10, 2026
f7ddc9e
apply foreground scope to svg/text node
noahchoii Jul 10, 2026
01fc728
fix overriding property
noahchoii Jul 10, 2026
11eb801
migrate semantic scale rule to determinism
noahchoii Jul 10, 2026
c9a8bff
primitive update
noahchoii Jul 10, 2026
ec8557f
버전 통일
noahchoii Jul 14, 2026
cdc8699
edit architecture docs
noahchoii Jul 14, 2026
7d11260
explicit requestId
noahchoii Jul 14, 2026
de1bc58
scrollbar overflow-x
noahchoii Jul 14, 2026
61e5aeb
show confidence & severity
noahchoii Jul 14, 2026
1144f88
include vitest.config.ts
noahchoii Jul 14, 2026
e0b533d
remove severity icon
noahchoii Jul 14, 2026
fd9df7b
align token text
noahchoii Jul 14, 2026
cfd4f34
add .env.example
noahchoii Jul 14, 2026
be79382
allow selecting instance
noahchoii Jul 22, 2026
89e23cb
ignore scanning vector node
noahchoii Jul 22, 2026
3861c7f
allow adding foreground color to icons
noahchoii Jul 22, 2026
1418e0f
ignore scanning hidden layer
noahchoii Jul 22, 2026
db77565
ignore using primitive colors
noahchoii Jul 22, 2026
0b0ffd6
add text node wcag 2.2
noahchoii Jul 22, 2026
5584acf
sticky header
noahchoii Jul 23, 2026
56cff5e
ignore scanning component instance
noahchoii Jul 23, 2026
f26c16e
reccomend to replace legacy tokens
noahchoii Jul 23, 2026
240934e
review color contrast of text nodes
noahchoii Jul 23, 2026
305c082
format & block self-cancle
noahchoii Jul 23, 2026
d1f2041
loader
noahchoii Jul 24, 2026
094416a
block self-cancle
noahchoii Jul 24, 2026
d10e0e6
success page
noahchoii Jul 24, 2026
3f206e7
format
noahchoii Jul 24, 2026
7a2528e
eanhance restart logic
noahchoii Jul 24, 2026
9b5fd4d
refactor scanning 💙 component
noahchoii Jul 27, 2026
68ffdce
ignore interaction layers
noahchoii Jul 27, 2026
41830c8
change selection when card clicked
noahchoii Jul 27, 2026
e2dab7d
refactor 💙 prefix & detached components
noahchoii Jul 27, 2026
a217bf5
adjust variable mode
noahchoii Jul 27, 2026
76c687c
refactor(schemas): extract NodeRef/TokenInfo bases; recompose 6 Usage…
noahchoii Jul 27, 2026
e9b928e
feat: add extraction rule types and node guards
noahchoii Jul 27, 2026
7b0dbfc
feat: add rule execution engine with trace and error isolation
noahchoii Jul 27, 2026
245e9fc
feat: add tokenField and composite rule factories
noahchoii Jul 27, 2026
cb20f2a
feat: add declarative space extraction rules
noahchoii Jul 27, 2026
ad6af34
feat: add declarative dimension and radius extraction rules
noahchoii Jul 27, 2026
71e190d
feat: split color extraction into declarative fill/stroke rules
noahchoii Jul 27, 2026
4f482b8
test: strengthen color rule TEXT-path assertions
noahchoii Jul 27, 2026
2038054
feat(extract): add rules/typography + rules/shadow
noahchoii Jul 27, 2026
5dc8c8b
refactor: replace imperative collectors with declarative rule registry
noahchoii Jul 27, 2026
e053aa4
fix: log rule extraction failures when trace is off
noahchoii Jul 27, 2026
06ed463
Merge branch 'main' into figma-token-review-plugin
noahchoii Jul 27, 2026
ea96c5b
format
noahchoii Jul 27, 2026
34f0abc
refactor(figma-token-review): move extract domain to models/
noahchoii Jul 27, 2026
aedbceb
feat: add plugin view and model layers
noahchoii Jul 27, 2026
59d5104
refactor: route plugin messages through MVC controllers
noahchoii Jul 27, 2026
1546199
refactor: align controller exports with plan contract
noahchoii Jul 28, 2026
631007e
chore: remove legacy handlers directory
noahchoii Jul 28, 2026
268af30
refactor: drop __testables escape hatch, colocate extract tests
noahchoii Jul 28, 2026
ab82215
docs: trim comments to essential constraints
noahchoii Jul 28, 2026
509354f
docs: update architecture doc for MVC plugin layout
noahchoii Jul 28, 2026
9cd7c11
format
noahchoii Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ CLAUDE.local.md
handoff.md
docs/research/
docs/plans/
docs/superpowers

# Worktrees
.worktrees
8 changes: 8 additions & 0 deletions apps/figma-token-review-plugin/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# LiteLLM proxy endpoint (Anthropic-compatible /v1/messages).
VITE_LITELLM_BASE_URL=https://litellm.internal.goorm.io

# LiteLLM API key.
VITE_LITELLM_API_KEY=

# Optional override; default is claude-sonnet-4-6.
VITE_LITELLM_MODEL=claude-sonnet-4-6
4 changes: 4 additions & 0 deletions apps/figma-token-review-plugin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.env.local
.env.*.local
dist/
node_modules/
162 changes: 162 additions & 0 deletions apps/figma-token-review-plugin/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Figma Token Review Plugin — 모듈 구조

```
src/
├── common/ # 두 사이드가 공유하는 wire 계약·도메인 스키마
├── plugin/ # Figma 샌드박스 (figma API 접근, DOM 없음)
└── ui/ # React iframe UI (feature-folder 기반)
```

- `plugin/`: 1단(결정론) RawExtract 추출. LLM 호출 없음.
- `ui/features/llm/`: 2단 LLM 판정. RawExtract → ScanPayload.
- 두 사이드는 `figma.ui.postMessage` / `window.message`로만 통신. 모든 wire 형식은 `common/`이 정의.

---

## `src/common/` — 공유 계약

- **`schemas.ts`** — 도메인 타입 (`Violation`, `EvaluateOutput`, `ScanPayload`, `SelectionState`, `RawExtract`).
- **`messages.ts`** — wire envelope (`UiEnvelope`, `CodeEnvelope`), `RequestId`, `postToCode`, `newRequestId`. request/response 상관 인프라(핸들러가 응답에 동일 id를 echo → stale 응답 차단).

---

## `src/plugin/` — Figma 샌드박스

`figma.*` API만 접근. DOM/window 없음. Vite가 `code.ts`를 entry로 단일 번들 생성.

- **`code.ts`** _(entry)_ — UI 표시 + 크기 복원 + 핸들러 init + 메시지 라우터 시작.
- **`messages.ts`** — `postToUi` 래퍼 + 핸들러 등록(`on(type, handler)`) + `figma.ui.onmessage` 부착. 응답에 requestId echo.
- **`handlers/`** — 메시지 종류별 핸들러.

| 파일 | 책임 |
| -------------- | ---------------------------------------------------------------------- |
| `selection.ts` | 초기 emit + `selectionchange` 구독 + `request-selection` 응답 |
| `scan.ts` | in-flight scan cancel, `extract.ts` 호출, 결과 echo |
| `extract.ts` | 프레임 추출 (`extractFrame`) |
| `focus.ts` | 노드 id 리스트 resolve, viewport 이동, missing 카운트 응답 |
| `resize.ts` | `figma.ui.resize` 적용. `commit===true`일 때만 `clientStorage` persist |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 resize.ts에서 commit에 대한 정보를 넘겨주지 않아서 항상 commit = false. 그리고 초기에도 항상 기본 사이즈로 plugin이 열리고 있어서 여기 내용 수정이 필요해 보입니다.!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 이건 제거한 부분인데 아키텍처 문서에 반영이 안되어 있었네요! 수정했습니다!


새 메시지 추가 = `handlers/<name>.ts` 1개 + `code.ts`에 init 한 줄.

---

## `src/ui/` — React iframe UI

`createRoot` 단일 진입점. React 19, Vite single-file 번들. 폴더 분류는 **layer가 아니라 feature** 단위.

```
ui/
├── App.tsx providers.tsx main.tsx index.css
├── assets/ # svg 등 정적 리소스
├── shared/ # cross-feature primitive (store factory 등)
├── features/ # 기능 단위 모듈
│ ├── messaging/ # plugin ↔ UI 와이어 통신 전담
│ ├── llm/ # LLM 호출 (2단 평가)
│ ├── scan/ # 스캔 상태 머신 + 훅
│ └── selection/ # 샌드박스 selection 상태 + 훅
├── components/ # 순수 재사용 UI
│ └── violation-card/
└── pages/ # 상태별 화면
```

새 기능 추가 = `features/<name>/` 폴더 하나만 늘어남. `src/ui/` 루트는 비대해지지 않음.

### 루트

- **`main.tsx`** _(entry)_ — `<StrictMode> → <ErrorBoundary> → <Providers> → <App>`. Render 트리만 조립.
- **`App.tsx`** — scan 상태 머신 switch 라우터. props drill 없음, store에서 직접 읽음.
- **`providers.tsx`** — `Toast.Provider` 래퍼 + `useBridge` + `useRequestSelection` 마운트.

### `shared/`

| 파일 | 책임 |
| ----------------- | ----------------------------------------------------------------------- |
| `create-store.ts` | 제네릭 store 팩토리 + `useStore` 훅. `Object.is` 동일 참조 시 알림 skip |

### `features/messaging/` — 와이어 통신

| 파일 | 책임 |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `bridge.ts` | `window.message` 단일 listener + `handle(msg)` 라우터. requestId mismatch drop. `useBridge` 훅 export |
| `evaluate.ts` | `extract-result` 메시지 받아 `runLlmEvaluation` 호출 후 scan store에 반영. AbortController로 in-flight evaluate 취소 |
| `focus.ts` | `requestFocus(nodeIds)` + `isActiveFocus()`. stale focus 토스트 차단 |
| `request-selection.ts` | 초기 selection 요청 effect |
| `index.ts` | barrel |

### `features/llm/` — LLM 평가 (2단)

| 파일 | 책임 |
| ----------- | ----------------------------------------------------------------------------------- |
| `client.ts` | LiteLLM HTTP 호출. 60s timeout, 429 1회 재시도. `LlmHttpError`/`LlmTimeoutError` |
| `parse.ts` | Anthropic content blocks → `ScanPayload` JSON parse + schema guard. `LlmParseError` |
| `prompt.ts` | `SYSTEM_PROMPT` + `buildRequest()` |
| `index.ts` | `runLlmEvaluation()` 공개 + env/model 해석 |

외부에서는 `runLlmEvaluation(extract, opts)` 한 함수만 사용. UI는 `messaging/evaluate.ts`만 의존.

### `features/scan/` — 스캔 상태 머신

| 파일 | 책임 |
| ------------- | ---------------------------------------------------------------------------------------------------- |
| `store.ts` | `idle/loading/clean/success` 머신 + `scanActions`. loading에 requestId 포함, action 단에서 race 가드 |
| `use-scan.ts` | scan state + `start(frameId, name)` (requestId 생성 + dispatch + postToCode) + `reset` |
| `index.ts` | barrel |

### `features/selection/` — 셀렉션 상태

| 파일 | 책임 |
| ------------------ | --------------------------- |
| `store.ts` | 샌드박스 selection 슬라이스 |
| `use-selection.ts` | selection store 구독 |
| `index.ts` | barrel |

### `pages/`

| 파일 | 책임 |
| ----------------- | ---------------------------------------------------------------------------------- |
| `home.tsx` | idle 화면. selection 종류에 따라 토스트/스캔 트리거 |
| `scan-result.tsx` | success 화면. Tabs(color/typography) + Violation 섹션. counts/sort/split `useMemo` |
| `success.tsx` | clean 화면 (위반 0) |

### `components/`

| 파일 | 책임 |
| -------------------- | --------------------------------------------------------------------------------- |
| `error-boundary.tsx` | 렌더 에러 catch + fallback UI + reset 버튼 |
| `resize-handler.tsx` | rAF throttle drag. pointermove → 큐 → next frame flush, pointerup만 `commit:true` |
| `toast.tsx` | `toastManager` 싱글톤 + ToastProvider |
| `hero-panel.tsx` | home/success 공통 패널 슬롯 |
| `loader.tsx` | loading 스피너 |

#### `components/violation-card/`

| 파일 | 책임 |
| -------------------------- | -------------------------------------------- |
| `violation-card.tsx` | 카드 컨테이너. 클릭 시 `requestFocus(nodes)` |
| `violation-breadcrumb.tsx` | 카드 상단 경로 표시 |
| `token-comparison.tsx` | used → suggested 토큰 chip + color swatch |
| `violation-detail.tsx` | detail 텍스트 + AI 추천 뱃지 |
| `utils.ts` | `isHexColor` 등 헬퍼 |
| `index.ts` | barrel |

---

## 데이터 흐름

```
[사용자 액션]
└─> features/scan.use-scan.start ─postToCode─> plugin/messages ─dispatch─> handlers/scan
└─ figma API
└─ postToUi(requestId)
<── window.message ── features/messaging/bridge (단일 listener)
└─ handle() ─ scan/selection store action (requestId 매칭 후 set)
└─ useSyncExternalStore notify
└─ React re-render
```

특별 경로

- **extract-result → LLM 평가**: `features/messaging/bridge.handle` → `evaluate.evaluateExtract` → `features/llm.runLlmEvaluation` → `scanActions.result`.
- **selection 변화**: plugin `figma.on('selectionchange')` → `handlers/selection` emit → `selectionStore` set → 모든 구독 컴포넌트.
- **focus 토스트**: bridge가 `isActiveFocus()`로 stale 차단.
- **resize**: UI rAF 큐 → pointermove 시 `commit` 없음 → sandbox apply only / pointerup commit → persist.
9 changes: 9 additions & 0 deletions apps/figma-token-review-plugin/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @ts-check
import { configs as reactPackage } from '@repo/eslint-config/react-package';

export default [
...reactPackage,
{
ignores: ['dist/**/*'],
},
];
12 changes: 12 additions & 0 deletions apps/figma-token-review-plugin/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vapor Token Review</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/ui/main.tsx"></script>
</body>
</html>
14 changes: 14 additions & 0 deletions apps/figma-token-review-plugin/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "Vapor Token Review",
"id": "1542812611331843954",
"api": "1.0.0",
"main": "code.js",
"ui": "index.html",
"capabilities": [],
"enableProposedApi": false,
"documentAccess": "dynamic-page",
"editorType": ["figma"],
"networkAccess": {
"allowedDomains": ["https://litellm.internal.goorm.io"]
}
}
42 changes: 42 additions & 0 deletions apps/figma-token-review-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "figma-token-review-plugin",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "rimraf dist && tsc -b && vite build --config vite.config.ui.ts && vite build --config vite.config.plugin.ts && cp manifest.json dist/",
"build:plugin": "vite build --config vite.config.plugin.ts",
"build:ui": "vite build --config vite.config.ui.ts",
"clean": "rimraf dist node_modules .turbo",
"dev": "vite build --watch --config vite.config.ui.ts & vite build --watch --config vite.config.plugin.ts",
"lint": "eslint ./src",
"predev:figma": "mkdir -p dist && cp manifest.json dist/",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc -b --noEmit"
},
"dependencies": {
"@vapor-ui/core": "workspace:*",
"@vapor-ui/icons": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

패키지 전체적인 라이브러리 버전 일치를 위해 catalog로 추가하는 것이 좋을 듯 합니다.!

},
"devDependencies": {
"@figma/plugin-typings": "^1.125.0",
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@tailwindcss/vite": "^4.0.0",
"@types/node": "^26.1.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
"@vitest/coverage-v8": "^4.1.10",
"eslint": "catalog:",
"rimraf": "^6.0.0",
"tailwindcss": "^4.0.0",
"typescript": "catalog:",
"vite": "^6.0.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 vite 6버전으로 하신 이유가 있으실까요? 최신 버전인 8.* 버전 혹은 다른 vite를 사용하는 앱의 버전과 일치시키면 좋을 것 같습니다.!

"vite-plugin-singlefile": "^2.0.0",
"vitest": "^4.1.10"
}
}
50 changes: 50 additions & 0 deletions apps/figma-token-review-plugin/src/common/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { LlmContext, RawExtract, SelectionState } from './schemas';

export type CodeMsg =
| { type: 'selection'; state: SelectionState }
| { type: 'extract-result'; payload: { extract: RawExtract; llmContext: LlmContext } }
| { type: 'extract-error'; message: string }
| { type: 'focus-result'; resolved: number; missing: number }
| { type: 'focus-error'; message: string };

export type UiMsg =
| { type: 'request-selection' }
| { type: 'scan'; frameId: string }
| { type: 'focus'; nodeIds: string[] }
| { type: 'resize'; width: number; height: number; commit?: boolean };

/* -------------------------------------------------------------------------------------------------
* Envelope + RequestId (formerly src/shared/protocol.ts)
* -----------------------------------------------------------------------------------------------*/

export type RequestId = string;

export type Envelope<T> = T & { requestId?: RequestId };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 타입이 scan, focus와 같이 requestId가 반드시 필요한 메시지에도 requestID가 optional로 들어가고 있는 것 같습니다.

Suggested change
export type Envelope<T> = T & { requestId?: RequestId };
type UiMsg =
| { type: 'scan'; frameId: string; requestId: RequestId }
| { type: 'focus'; nodeIds: string[]; requestId: RequestId }
| { type: 'request-selection' }
| { type: 'resize'; width: number; height: number }
| { type: 'api-key:get' }
| { type: 'api-key:set'; value: string }
| { type: 'api-key:clear' };

이렇게 msg에서 바로 타입을 명확하게 정의하는 것도 좋아보입니다.!


export type UiEnvelope = Envelope<UiMsg>;

export type CodeEnvelope = Envelope<CodeMsg>;

export function newRequestId(): RequestId {
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
}

/* -------------------------------------------------------------------------------------------------
* Post helpers
* -----------------------------------------------------------------------------------------------*/

/**
* UI → plugin direction. Uses `parent` (DOM iframe global).
* Tree-shaken from plugin bundle when only types are imported.
*/
export function postToCode(msg: UiMsg | UiEnvelope): void {
parent.postMessage({ pluginMessage: msg }, '*');
}

/**
* Plugin → UI direction. Uses `figma.ui` (plugin sandbox global).
* Tree-shaken from UI bundle when only types are imported.
*/
export function postToUi(msg: CodeEnvelope, requestId?: RequestId): void {
figma.ui.postMessage(requestId ? { ...msg, requestId } : msg);
}
Loading
Loading