@mp3wizard/figma-console-mcp 1.32.3 → 1.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -17
- package/dist/cloudflare/core/cloud-websocket-connector.js +18 -0
- package/dist/cloudflare/core/design-system-manifest.js +19 -14
- package/dist/cloudflare/core/design-system-tools.js +43 -34
- package/dist/cloudflare/core/diagnose-tool.js +4 -0
- package/dist/cloudflare/core/enrichment/enrichment-service.js +11 -5
- package/dist/cloudflare/core/enrichment/style-resolver.js +38 -18
- package/dist/cloudflare/core/figma-api.js +118 -54
- package/dist/cloudflare/core/figma-tools.js +179 -63
- package/dist/cloudflare/core/port-discovery.js +404 -31
- package/dist/cloudflare/core/tokens/alias-resolver.js +75 -5
- package/dist/cloudflare/core/tokens/config.js +10 -0
- package/dist/cloudflare/core/tokens/dialect.js +232 -0
- package/dist/cloudflare/core/tokens/figma-converter.js +144 -16
- package/dist/cloudflare/core/tokens/formatters/css-vars.js +21 -12
- package/dist/cloudflare/core/tokens/formatters/dtcg.js +106 -30
- package/dist/cloudflare/core/tokens/formatters/json.js +28 -10
- package/dist/cloudflare/core/tokens/formatters/scss.js +19 -13
- package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +15 -9
- package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +14 -9
- package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +11 -5
- package/dist/cloudflare/core/tokens/index.js +2 -1
- package/dist/cloudflare/core/tokens/parsers/dtcg.js +32 -5
- package/dist/cloudflare/core/tokens/schemas.js +4 -0
- package/dist/cloudflare/core/tokens-tools.js +1017 -88
- package/dist/cloudflare/core/version-tools.js +44 -3
- package/dist/cloudflare/core/websocket-connector.js +42 -0
- package/dist/cloudflare/core/websocket-server.js +99 -8
- package/dist/cloudflare/core/write-tools.js +355 -86
- package/dist/cloudflare/index.js +7 -7
- package/dist/core/design-system-manifest.d.ts +1 -0
- package/dist/core/design-system-manifest.d.ts.map +1 -1
- package/dist/core/design-system-manifest.js +19 -14
- package/dist/core/design-system-manifest.js.map +1 -1
- package/dist/core/design-system-tools.d.ts.map +1 -1
- package/dist/core/design-system-tools.js +43 -34
- package/dist/core/design-system-tools.js.map +1 -1
- package/dist/core/diagnose-tool.d.ts +8 -0
- package/dist/core/diagnose-tool.d.ts.map +1 -1
- package/dist/core/diagnose-tool.js +4 -0
- package/dist/core/diagnose-tool.js.map +1 -1
- package/dist/core/enrichment/enrichment-service.d.ts.map +1 -1
- package/dist/core/enrichment/enrichment-service.js +11 -5
- package/dist/core/enrichment/enrichment-service.js.map +1 -1
- package/dist/core/enrichment/style-resolver.d.ts +7 -2
- package/dist/core/enrichment/style-resolver.d.ts.map +1 -1
- package/dist/core/enrichment/style-resolver.js +38 -18
- package/dist/core/enrichment/style-resolver.js.map +1 -1
- package/dist/core/figma-api.d.ts +18 -9
- package/dist/core/figma-api.d.ts.map +1 -1
- package/dist/core/figma-api.js +118 -54
- package/dist/core/figma-api.js.map +1 -1
- package/dist/core/figma-connector.d.ts +12 -0
- package/dist/core/figma-connector.d.ts.map +1 -1
- package/dist/core/figma-tools.d.ts.map +1 -1
- package/dist/core/figma-tools.js +179 -63
- package/dist/core/figma-tools.js.map +1 -1
- package/dist/core/port-discovery.d.ts +40 -0
- package/dist/core/port-discovery.d.ts.map +1 -1
- package/dist/core/port-discovery.js +404 -31
- package/dist/core/port-discovery.js.map +1 -1
- package/dist/core/tokens/alias-resolver.d.ts +45 -3
- package/dist/core/tokens/alias-resolver.d.ts.map +1 -1
- package/dist/core/tokens/alias-resolver.js +75 -5
- package/dist/core/tokens/alias-resolver.js.map +1 -1
- package/dist/core/tokens/config.d.ts +28 -0
- package/dist/core/tokens/config.d.ts.map +1 -1
- package/dist/core/tokens/config.js +10 -0
- package/dist/core/tokens/config.js.map +1 -1
- package/dist/core/tokens/dialect.d.ts +107 -0
- package/dist/core/tokens/dialect.d.ts.map +1 -0
- package/dist/core/tokens/dialect.js +233 -0
- package/dist/core/tokens/dialect.js.map +1 -0
- package/dist/core/tokens/figma-converter.d.ts +23 -2
- package/dist/core/tokens/figma-converter.d.ts.map +1 -1
- package/dist/core/tokens/figma-converter.js +144 -16
- package/dist/core/tokens/figma-converter.js.map +1 -1
- package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -1
- package/dist/core/tokens/formatters/css-vars.js +21 -12
- package/dist/core/tokens/formatters/css-vars.js.map +1 -1
- package/dist/core/tokens/formatters/dtcg.d.ts +2 -2
- package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -1
- package/dist/core/tokens/formatters/dtcg.js +106 -30
- package/dist/core/tokens/formatters/dtcg.js.map +1 -1
- package/dist/core/tokens/formatters/json.d.ts.map +1 -1
- package/dist/core/tokens/formatters/json.js +28 -10
- package/dist/core/tokens/formatters/json.js.map +1 -1
- package/dist/core/tokens/formatters/scss.d.ts.map +1 -1
- package/dist/core/tokens/formatters/scss.js +19 -13
- package/dist/core/tokens/formatters/scss.js.map +1 -1
- package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -1
- package/dist/core/tokens/formatters/style-dictionary-v3.js +15 -9
- package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -1
- package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -1
- package/dist/core/tokens/formatters/tailwind-v4.js +14 -9
- package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -1
- package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -1
- package/dist/core/tokens/formatters/tokens-studio.js +11 -5
- package/dist/core/tokens/formatters/tokens-studio.js.map +1 -1
- package/dist/core/tokens/index.d.ts +2 -1
- package/dist/core/tokens/index.d.ts.map +1 -1
- package/dist/core/tokens/index.js +2 -1
- package/dist/core/tokens/index.js.map +1 -1
- package/dist/core/tokens/parsers/dtcg.js +32 -5
- package/dist/core/tokens/parsers/dtcg.js.map +1 -1
- package/dist/core/tokens/schemas.d.ts +3 -0
- package/dist/core/tokens/schemas.d.ts.map +1 -1
- package/dist/core/tokens/schemas.js +4 -0
- package/dist/core/tokens/schemas.js.map +1 -1
- package/dist/core/tokens/types.d.ts +57 -1
- package/dist/core/tokens/types.d.ts.map +1 -1
- package/dist/core/tokens/types.js.map +1 -1
- package/dist/core/tokens-tools.d.ts +250 -7
- package/dist/core/tokens-tools.d.ts.map +1 -1
- package/dist/core/tokens-tools.js +1017 -88
- package/dist/core/tokens-tools.js.map +1 -1
- package/dist/core/version-tools.d.ts.map +1 -1
- package/dist/core/version-tools.js +44 -3
- package/dist/core/version-tools.js.map +1 -1
- package/dist/core/websocket-connector.d.ts +38 -0
- package/dist/core/websocket-connector.d.ts.map +1 -1
- package/dist/core/websocket-connector.js +42 -0
- package/dist/core/websocket-connector.js.map +1 -1
- package/dist/core/websocket-server.d.ts +23 -0
- package/dist/core/websocket-server.d.ts.map +1 -1
- package/dist/core/websocket-server.js +99 -8
- package/dist/core/websocket-server.js.map +1 -1
- package/dist/core/write-tools.d.ts.map +1 -1
- package/dist/core/write-tools.js +355 -86
- package/dist/core/write-tools.js.map +1 -1
- package/dist/local.d.ts +0 -1
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +253 -63
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +382 -28
- package/figma-desktop-bridge/ui.html +578 -292
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
|
|
9
9
|
> **Your design system as an API.** Model Context Protocol server that bridges design and development—giving AI assistants complete access to Figma for **extraction**, **creation**, **debugging**, and **bidirectional token sync**.
|
|
10
10
|
|
|
11
|
-
> **🆕
|
|
11
|
+
> **🆕 Bidirectional Token Sync v2 + DTCG 2025.10 (v1.34.0):** `figma_import_tokens` now applies the *whole* diff plan — creates, renames, alias re-targeting, and replace-gated deletes — for a true code→Figma round-trip; exports speak DTCG 2025.10 on request (opt-in, legacy default byte-identical); scopes/codeSyntax round-trip; `figma_setup_design_tokens` accepts alias values; and the new `figma_create_component_set` builds a full variant set from an axes matrix in one call. **Plugin re-import required** (`code.js` + `ui.html` changed). [See what's new →](CHANGELOG.md#1340---2026-07-03)
|
|
12
12
|
|
|
13
13
|
## What is this?
|
|
14
14
|
|
|
15
15
|
Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
16
16
|
|
|
17
17
|
- **🎨 Design system extraction** - Pull variables, components, and styles
|
|
18
|
-
- **🔁 Bidirectional token sync** - Export Figma variables to DTCG JSON +
|
|
18
|
+
- **🔁 Bidirectional token sync** - Export Figma variables to DTCG JSON (legacy or 2025.10 dialect) + 9 more formats; push code-side edits back to Figma with full apply — creates, renames, alias re-targeting, and replace-gated deletes. Replaces Style Dictionary and Tokens Studio's export pipeline.
|
|
19
19
|
- **📸 Visual debugging** - Take screenshots for context
|
|
20
|
-
- **✏️ Design creation** - Create UI components, frames, and layouts directly in Figma
|
|
20
|
+
- **✏️ Design creation** - Create UI components, frames, and layouts directly in Figma — including one-call component-set creation from a variant axes matrix
|
|
21
21
|
- **🔧 Variable management** - Create, update, rename, and delete design tokens
|
|
22
22
|
- **🕰 Version history & time-series awareness** - List versions, diff snapshots, generate markdown changelogs, trace property/variant introduction via binary-search blame
|
|
23
23
|
- **⚡ Real-time monitoring** - Watch console logs from the Desktop Bridge plugin
|
|
@@ -55,9 +55,9 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
|
55
55
|
| Real-time monitoring (console, selection) | ✅ | ❌ | ❌ |
|
|
56
56
|
| Desktop Bridge plugin | ✅ | ✅ | ❌ |
|
|
57
57
|
| Requires Node.js | Yes | **No** | No |
|
|
58
|
-
| **Total tools available** | **
|
|
58
|
+
| **Total tools available** | **107** | **96** | **9** |
|
|
59
59
|
|
|
60
|
-
> **Bottom line:** Remote SSE is **read-only** with
|
|
60
|
+
> **Bottom line:** Remote SSE is **read-only** with 9 tools. **Cloud Mode** unlocks write access (96 tools) from web AI clients without Node.js. NPX/Local Git gives the full 107 tools with real-time monitoring.
|
|
61
61
|
|
|
62
62
|
---
|
|
63
63
|
|
|
@@ -65,7 +65,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
|
65
65
|
|
|
66
66
|
**Best for:** Designers who want full AI-assisted design capabilities.
|
|
67
67
|
|
|
68
|
-
**What you get:** All
|
|
68
|
+
**What you get:** All 107 tools including design creation, variable management, and component instantiation.
|
|
69
69
|
|
|
70
70
|
#### Prerequisites
|
|
71
71
|
|
|
@@ -162,7 +162,7 @@ Create a simple frame with a blue background
|
|
|
162
162
|
|
|
163
163
|
**Best for:** Developers who want to modify source code or contribute to the project.
|
|
164
164
|
|
|
165
|
-
**What you get:** Same
|
|
165
|
+
**What you get:** Same 107 tools as NPX, plus full source code access.
|
|
166
166
|
|
|
167
167
|
#### Quick Setup
|
|
168
168
|
|
|
@@ -251,7 +251,7 @@ Ready for design creation? Follow the [NPX Setup](#-npx-setup-recommended) guide
|
|
|
251
251
|
|
|
252
252
|
**Best for:** Using Claude.ai, v0, Replit, or Lovable to create and modify Figma designs — no Node.js required.
|
|
253
253
|
|
|
254
|
-
**What you get:**
|
|
254
|
+
**What you get:** 96 tools including full write access — design creation, variable management, component instantiation, and all REST API tools. Only real-time monitoring (console logs, selection tracking, document changes) requires Local Mode.
|
|
255
255
|
|
|
256
256
|
#### Prerequisites
|
|
257
257
|
|
|
@@ -308,7 +308,7 @@ AI Client → Cloud MCP Server → Durable Object Relay → Desktop Bridge Plugi
|
|
|
308
308
|
| Feature | NPX (Recommended) | Cloud Mode | Local Git | Remote SSE |
|
|
309
309
|
|---------|-------------------|------------|-----------|------------|
|
|
310
310
|
| **Setup time** | ~10 minutes | ~5 minutes | ~15 minutes | ~2 minutes |
|
|
311
|
-
| **Total tools** | **
|
|
311
|
+
| **Total tools** | **107** | **96** | **107** | **9** (read-only) |
|
|
312
312
|
| **Design creation** | ✅ | ✅ | ✅ | ❌ |
|
|
313
313
|
| **Variable management** | ✅ | ✅ | ✅ | ❌ |
|
|
314
314
|
| **Component instantiation** | ✅ | ✅ | ✅ | ❌ |
|
|
@@ -323,7 +323,7 @@ AI Client → Cloud MCP Server → Durable Object Relay → Desktop Bridge Plugi
|
|
|
323
323
|
| **Automatic updates** | ✅ (`@latest`) | ✅ | Manual (`git pull`) | ✅ |
|
|
324
324
|
| **Source code access** | ❌ | ❌ | ✅ | ❌ |
|
|
325
325
|
|
|
326
|
-
> **Key insight:** Remote SSE is read-only. Cloud Mode adds write access for web AI clients without Node.js. NPX/Local Git give the full
|
|
326
|
+
> **Key insight:** Remote SSE is read-only. Cloud Mode adds write access for web AI clients without Node.js. NPX/Local Git give the full 107 tools.
|
|
327
327
|
|
|
328
328
|
**📖 [Complete Feature Comparison](docs/mode-comparison.md)**
|
|
329
329
|
|
|
@@ -415,6 +415,10 @@ When you first use design system tools:
|
|
|
415
415
|
- Create frames, shapes, text, components
|
|
416
416
|
- Apply auto-layout, styles, effects
|
|
417
417
|
- Build complete UI mockups programmatically
|
|
418
|
+
- `figma_create_component_set` - **Create a component set with variants in one declarative call**
|
|
419
|
+
- Generate every variant combination from an axes matrix (e.g. `{ State: ["default", "hover", "disabled"], Size: ["sm", "lg"] }` → 6 variants) off a base component, or combine existing components
|
|
420
|
+
- `Prop=Value` variant naming, `combineAsVariants` under the hood, optional auto-arranged labeled grid
|
|
421
|
+
- Returns each variant's key, ready for `figma_instantiate_component`
|
|
418
422
|
- `figma_arrange_component_set` - **Organize variants into professional component sets**
|
|
419
423
|
- Convert multiple component variants into a proper Figma component set
|
|
420
424
|
- Applies native purple dashed border visualization automatically
|
|
@@ -432,8 +436,8 @@ When you first use design system tools:
|
|
|
432
436
|
- `figma_generate_component_doc` - Generate platform-agnostic markdown documentation by merging Figma design data with code-side info
|
|
433
437
|
|
|
434
438
|
### 🔁 Token Sync (Local Mode + Cloud Mode)
|
|
435
|
-
- `figma_export_tokens` - **Export Figma variables to design token files in your codebase.** Canonical DTCG JSON
|
|
436
|
-
- `figma_import_tokens` - **Push code-side token edits back to Figma.**
|
|
439
|
+
- `figma_export_tokens` - **Export Figma variables to design token files in your codebase.** Canonical DTCG JSON (legacy hex dialect by default, or DTCG 2025.10 object colors/dimensions via `dtcgDialect: "2025"`) plus CSS, Tailwind v4/v3, SCSS, TS, JSON, Style Dictionary, and Tokens Studio formats. Diff-aware merge against existing source files (only writes what changed). `tokens.config.json` autodiscovery means zero-arg calls after first setup. Scopes and codeSyntax metadata round-trip via `$extensions`. Replaces Style Dictionary and Tokens Studio's export pipeline for popular styling methods.
|
|
440
|
+
- `figma_import_tokens` - **Push code-side token edits back to Figma with a full apply phase.** Diffs against current Figma state, then applies value updates, **creates** missing collections/variables, applies **renames**, writes real **alias** (`VARIABLE_ALIAS`) references, and — only under `strategy: "replace"` — deletes Figma-only variables. Round-trip safe — Figma variable IDs preserved in DTCG `$extensions["figma-console-mcp"]` so renames on either side don't create duplicates. Accepts both DTCG dialects. Dry-run strategy for safe previews. In Cloud Mode, pass tokens inline via `payload` or `files` (no local filesystem access).
|
|
437
441
|
|
|
438
442
|
### 🔧 Variable Management (Local Mode + Cloud Mode)
|
|
439
443
|
- `figma_create_variable_collection` - Create new variable collections with modes
|
|
@@ -446,7 +450,7 @@ When you first use design system tools:
|
|
|
446
450
|
- `figma_rename_mode` - Rename existing modes
|
|
447
451
|
- `figma_batch_create_variables` - Create up to 100 variables in one call (10-50x faster)
|
|
448
452
|
- `figma_batch_update_variables` - Update up to 100 variable values in one call
|
|
449
|
-
- `figma_setup_design_tokens` - Create complete token system (collection + modes + variables) atomically
|
|
453
|
+
- `figma_setup_design_tokens` - Create complete token system (collection + modes + variables) atomically — values accept DTCG brace references (`"{color.blue.600}"`) that resolve to real variable aliases
|
|
450
454
|
|
|
451
455
|
### 📌 FigJam Board Tools (Local Mode + Cloud Mode)
|
|
452
456
|
- `figjam_create_sticky` - Create a sticky note with color options
|
|
@@ -669,7 +673,7 @@ The **Figma Desktop Bridge** plugin is the recommended way to connect Figma to t
|
|
|
669
673
|
- The MCP server communicates via **WebSocket** through the Desktop Bridge plugin
|
|
670
674
|
- The server tries port 9223 first, then automatically falls back through ports 9224–9232 if needed
|
|
671
675
|
- The plugin scans all ports in the range and connects to every active server it finds
|
|
672
|
-
- All
|
|
676
|
+
- All 107 tools work through the WebSocket transport
|
|
673
677
|
|
|
674
678
|
**Multiple files:** The WebSocket server supports multiple simultaneous plugin connections — one per open Figma file. Each connection is tracked by file key with independent state (selection, document changes, console logs).
|
|
675
679
|
|
|
@@ -808,9 +812,13 @@ The architecture supports adding new apps with minimal boilerplate — each app
|
|
|
808
812
|
|
|
809
813
|
## 🛤️ Roadmap
|
|
810
814
|
|
|
811
|
-
**Current Status:** v1.
|
|
815
|
+
**Current Status:** v1.34.0 (Stable) - Production-ready. Latest: Bidirectional Token Sync v2 + DTCG 2025.10 — `figma_import_tokens` now applies the complete diff plan (creates missing collections/variables, applies renames, writes real `VARIABLE_ALIAS` references, and deletes only under explicit `replace`), `figma_export_tokens` speaks the DTCG 2025.10 dialect on request (legacy default byte-identical), variable scopes/codeSyntax round-trip via `$extensions`, `figma_setup_design_tokens` accepts alias values via DTCG brace references, and the new `figma_create_component_set` builds a full variant set from an axes matrix in one call. On top of the v1.33.x line: version-handshake fix (re-import banner only fires when plugin files actually changed), security dependency sweep, and the v1.33.0 connection UX overhaul (honest status pill derived from live connection state, `/health` auto-discovery with self-healing reconnect) + a 33-fix full-codebase audit (lossless DTCG multi-mode round-trips, cross-collection alias resolution, branch-URL correctness across REST tools, cache-poisoning and CSWSH fixes, bridge-first screenshots). Built on WCAG-accurate accessibility auditing (line height below 1.5× is no longer mis-flagged as a failure; readability hints decoupled from conformance checks and scoped to multi-line text; code-side WCAG 1.4.12 check), a self-healing Desktop Bridge connection (zombie-process reaper + auto-reconnect watchdog — fixes the recurring "not connected until restart" bug), native variable binding on fills/strokes + typography control in the write tools, shared-library inspection (key-based component resolution + library variable read/import without Enterprise plan), 10-format token export pipeline (DTCG, CSS, Tailwind v4, Tailwind v3, SCSS, TS module, JSON flat/nested, Style Dictionary v3, Tokens Studio), bidirectional Figma↔code token sync, version history & time-series awareness, FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, **107 tools** (Local) / **96 tools** (Cloud) / **9 tools** (Remote read-only), Comments API, cross-MCP identity disambiguation, and MCP Apps.
|
|
812
816
|
|
|
813
817
|
**Recent Releases:**
|
|
818
|
+
- [x] **v1.34.0** - Bidirectional Token Sync v2 + DTCG 2025.10. `figma_import_tokens` now applies the *complete* diff plan: missing collections and variables are created (with modes, inferred/recorded types, and values set in dependency order — aliases in a second pass), token-path renames route to the update phase by round-trip variable ID (no more create+delete pairs that would permanently destroy the original under `replace`), reference values write real `{ type: "VARIABLE_ALIAS", id }` payloads via a four-tier resolver, and deletes are strictly gated behind `strategy: "replace"`. `figma_export_tokens` gains `dtcgDialect: "2025"` (object-form colors from full-precision floats, object dimensions) while the legacy default stays byte-identical; import accepts both dialects unconditionally with dialect-insensitive diff normalization. Variable `scopes` + `codeSyntax` round-trip through `$extensions["figma-console-mcp"]`. `figma_setup_design_tokens` accepts DTCG brace references (`"{color.blue.600}"`) that resolve to real aliases, including forward references. New tool `figma_create_component_set` builds a variant set from an axes matrix (or combines existing components) with `Prop=Value` naming, optional auto-arranged grid, and variant keys in the response — with count-scaled timeouts and rollback on failure. **Plugin re-import required** (`code.js` + `ui.html` changed — the component-set handler and relay). 183 tests across the token/write-tools suites.
|
|
819
|
+
- [x] **v1.33.2** - Version-handshake false-positive fix. The v1.33.0 handshake compared the plugin's reported version against the server's *package* version, so server-only releases (like the v1.33.1 dependency sweep) flagged every up-to-date plugin as stale and pushed the re-import banner for files that hadn't changed. The server now compares against the `PLUGIN_VERSION` embedded in the `figma-desktop-bridge/code.js` it ships — exactly what a re-import would install — and `PLUGIN_VERSION` itself now means "last release in which plugin files changed" (release tooling bumps it only when `figma-desktop-bridge/` actually changed since the last tag). `figma_get_status` gains `transport.websocket.bundledPluginVersion`; `figma_diagnose` blames the right version. No new tools, **no plugin re-import required** (one-time exception: if you re-imported at v1.33.1, the banner appears once more — clear it with one final re-import). 1245 tests passing (9 new).
|
|
820
|
+
- [x] **v1.33.1** - Security dependency sweep. All runtime and critical npm audit alerts resolved via in-range bumps (`ws` 8.21.0, `hono` 4.12.27, `undici` 7.28.0, `handlebars` 4.7.9 — the lone critical, dev-only — plus `lodash`, `path-to-regexp`, `basic-ftp`, `fast-uri`, `vite`). `wrangler` deliberately held at 4.72.0 because newer versions require Node ≥22; the only residual audit findings are inside wrangler/miniflare's dev-time toolchain, which never ships in the npm package or Worker bundle. Supersedes dependabot PRs #81/#82/#84. No code changes, no API changes, no plugin re-import. 1236 tests passing unchanged.
|
|
821
|
+
- [x] **v1.33.0** - Connection UX overhaul + full-codebase audit. The plugin's status pill now derives from live connection state instead of Figma's variables loading (it used to glow green with zero MCP servers connected); HTTP `/health` auto-discovery reconnects restarted servers automatically (including one-dead-among-live, previously a permanent dead end); a version handshake banners the plugin UI when a re-import is needed and surfaces the mismatch in `figma_get_status`/`figma_diagnose`; cloud pairing config survives plugin reopen and its status line is derived + labeled (no more orphaned "Disconnected" under a green pill); all plugin copy is designer-language. The audit fixed 33 verified issues: lossless DTCG multi-mode round-trips, set-qualified cross-collection aliases, TIMING/EASING mapped to DTCG `duration`/`cubicBezier`, two cache-poisoning bugs (the "search returns 0 components" reports), a CSWSH origin bypass (`startsWith` → exact match), post-sleep reaper kill-safety (plus a shell-free `/health` probe with `os.devNull` so Windows curl can't false-negative a healthy sibling), branch-URL correctness across REST tools, and bridge-first `figma_take_screenshot`. `figma_arrange_component_set` now rearranges variants in place so placed instances survive. No new tools; **plugin re-import required** (`code.js` + `ui.html` changed — and the new handshake makes this the last one you have to discover on your own). 1236 tests passing (33 new).
|
|
814
822
|
- [x] **v1.32.1** - Documentation-generator fix reported by Robin Di Capua: `figma_generate_component_doc` documented **colors** as raw hex (with `—` in the Figma Variable column) even when fills/strokes were bound to variables, while spacing tokens documented correctly. Two root causes — an id→name lookup that read the wrong keys (`.id`/`.name` instead of `variableId`/`variableName`), and variable names only ever being sourced from the Enterprise-only REST `/variables/local` endpoint (403 elsewhere). The generator now resolves names via the Desktop Bridge Plugin API (works on every plan) and threads them through the States, Color Tokens, and Spacing tables, so real token names like `color/content/default` and `spacing/1` appear. No new tools, no arg-shape changes, no plugin re-import required. 1203 tests passing.
|
|
815
823
|
- [x] **v1.32.0** - Accessibility-audit correctness fix reported by Isabella (a11y collaborator): `figma_lint_design` was flagging line height below 1.5× as an accessibility failure on hundreds of components. That misreads **WCAG 1.4.12 Text Spacing**, which requires content to *support* user spacing overrides without loss — not that designs *ship* at 1.5× — so a sub-1.5 line height is not a conformance failure. Line/paragraph-spacing checks are now scoped to multi-line text (single-line labels and buttons exempt); readability hints (`text-size`, `line-height`, `letter-spacing`, `paragraph-spacing`) are decoupled from the `wcag` group into an opt-in `best-practice` group, so the default audit (`['wcag','design-system','layout']`) and `rules: ['wcag']` return genuine conformance only; and a new code-side `text-spacing-support` advisory in `figma_scan_code_accessibility` flags fixed-px typography — where 1.4.12/1.4.4 are actually verifiable. No new tools, no arg-shape changes; **plugin re-import required** to pick up the new audit behavior (bridge protocol unchanged, so an un-updated plugin stays compatible). 1196 tests passing.
|
|
816
824
|
- [x] **v1.31.0** - Fixes the most-reported reliability bug: the Desktop Bridge connection dropping and staying down until you closed the plugin, restarted your MCP client, or killed ports by hand. Root cause was **zombie MCP server processes** squatting the WebSocket port range (9223–9232) after a bad shutdown. The reaper now escalates `SIGTERM` → `SIGKILL` (a hung server that ignores graceful shutdown can no longer survive), sweeps the range every 5 minutes via an `unref`'d periodic reaper, and a shutdown backstop prevents a server from zombifying in the first place. The redesigned Desktop Bridge plugin adds an auto-reconnect watchdog (re-probes every ~12s while disconnected), a context-aware **Pause / Resume / Reconnect** button, and a live server-count badge. No new tools; **plugin re-import required** (bridge `ui.html` + `code.js` changed). 1190 tests passing, including an integration test that spawns a real `SIGTERM`-ignoring process and asserts the reaper kills it.
|
|
@@ -836,10 +844,10 @@ The architecture supports adding new apps with minimal boilerplate — each app
|
|
|
836
844
|
- [x] **v1.7.0** - MCP Apps (Token Browser, Design System Dashboard), batch variable operations, design-code parity tools.
|
|
837
845
|
|
|
838
846
|
**Coming Next:**
|
|
839
|
-
- [ ] **Token sync —
|
|
847
|
+
- [ ] **Token sync — non-DTCG input parsers** - Parsers for non-DTCG input (Tokens Studio, CSS vars, Tailwind v4, Tailwind v3 config, SCSS, Style Dictionary v3, JSON flat/nested) so `figma_import_tokens` can ingest the same formats it exports. (The import-side apply expansion — creates, replace-gated deletes, alias-target updates — shipped in v1.34.0.)
|
|
848
|
+
- [ ] **Cross-library variable resolution** - Resolve cross-library aliases via `getVariableByIdAsync` so they render as real `var(--target)` references in exports instead of comments.
|
|
840
849
|
- [ ] **Component template library** - Common UI pattern generation
|
|
841
850
|
- [ ] **Visual regression testing** - Screenshot diff capabilities
|
|
842
|
-
- [ ] **Design linting** - Automated compliance and accessibility checks
|
|
843
851
|
|
|
844
852
|
**📖 [Full Roadmap](docs/ROADMAP.md)**
|
|
845
853
|
|
|
@@ -180,6 +180,24 @@ export class CloudWebSocketConnector {
|
|
|
180
180
|
}
|
|
181
181
|
return this.sendCommand('INSTANTIATE_COMPONENT', params);
|
|
182
182
|
}
|
|
183
|
+
async createComponentSet(params) {
|
|
184
|
+
// Timeout scales with variant count — the plugin builds all variants in
|
|
185
|
+
// one uncancellable pass, so a fixed 30s ceiling on big matrices reports
|
|
186
|
+
// failure while the set still gets created (retry → duplicates).
|
|
187
|
+
// Mirrors componentSetTimeoutMs in websocket-connector.ts (not imported:
|
|
188
|
+
// that module pulls the Node 'ws' stack into the Workers bundle).
|
|
189
|
+
let variantCount = 1;
|
|
190
|
+
if (params.componentIds?.length) {
|
|
191
|
+
variantCount = params.componentIds.length;
|
|
192
|
+
}
|
|
193
|
+
else if (params.properties) {
|
|
194
|
+
for (const values of Object.values(params.properties)) {
|
|
195
|
+
variantCount *= Math.max(1, values?.length ?? 1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const timeout = Math.min(120000, Math.max(30000, variantCount * 1200)) + 5000;
|
|
199
|
+
return this.sendCommand('CREATE_COMPONENT_SET', params, timeout);
|
|
200
|
+
}
|
|
183
201
|
// ============================================================================
|
|
184
202
|
// Node manipulation
|
|
185
203
|
// ============================================================================
|
|
@@ -68,12 +68,13 @@ export function searchComponents(manifest, query, options) {
|
|
|
68
68
|
const queryLower = query.toLowerCase();
|
|
69
69
|
const categoryLower = options?.category?.toLowerCase();
|
|
70
70
|
const allResults = [];
|
|
71
|
-
// Search component sets first (they're typically the main design system components)
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
// Search component sets first (they're typically the main design system components).
|
|
72
|
+
// Manifest maps are keyed by component key — always read names from the entry.
|
|
73
|
+
for (const compSet of Object.values(manifest.componentSets)) {
|
|
74
|
+
const nameLower = compSet.name.toLowerCase();
|
|
74
75
|
const descLower = compSet.description?.toLowerCase() || '';
|
|
75
76
|
const matchesQuery = !query || nameLower.includes(queryLower) || descLower.includes(queryLower);
|
|
76
|
-
const matchesCategory = !categoryLower || inferCategory(name).toLowerCase().includes(categoryLower);
|
|
77
|
+
const matchesCategory = !categoryLower || inferCategory(compSet.name).toLowerCase().includes(categoryLower);
|
|
77
78
|
if (matchesQuery && matchesCategory) {
|
|
78
79
|
allResults.push({
|
|
79
80
|
name: compSet.name,
|
|
@@ -81,17 +82,17 @@ export function searchComponents(manifest, query, options) {
|
|
|
81
82
|
nodeId: compSet.nodeId,
|
|
82
83
|
type: 'componentSet',
|
|
83
84
|
description: compSet.description,
|
|
84
|
-
category: inferCategory(name),
|
|
85
|
+
category: inferCategory(compSet.name),
|
|
85
86
|
variantCount: compSet.variants?.length || 0,
|
|
86
87
|
});
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
// Then search standalone components
|
|
90
|
-
for (const
|
|
91
|
-
const nameLower = name.toLowerCase();
|
|
91
|
+
for (const comp of Object.values(manifest.components)) {
|
|
92
|
+
const nameLower = comp.name.toLowerCase();
|
|
92
93
|
const descLower = comp.description?.toLowerCase() || '';
|
|
93
94
|
const matchesQuery = !query || nameLower.includes(queryLower) || descLower.includes(queryLower);
|
|
94
|
-
const matchesCategory = !categoryLower || inferCategory(name).toLowerCase().includes(categoryLower);
|
|
95
|
+
const matchesCategory = !categoryLower || inferCategory(comp.name).toLowerCase().includes(categoryLower);
|
|
95
96
|
if (matchesQuery && matchesCategory) {
|
|
96
97
|
allResults.push({
|
|
97
98
|
name: comp.name,
|
|
@@ -99,13 +100,17 @@ export function searchComponents(manifest, query, options) {
|
|
|
99
100
|
nodeId: comp.nodeId,
|
|
100
101
|
type: 'component',
|
|
101
102
|
description: comp.description,
|
|
102
|
-
category: inferCategory(name),
|
|
103
|
+
category: inferCategory(comp.name),
|
|
103
104
|
defaultSize: comp.defaultSize,
|
|
104
105
|
});
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
const total = allResults.length;
|
|
108
|
-
|
|
109
|
+
// Truncate long doc blocks in search hits — full text is available via
|
|
110
|
+
// figma_get_component_details.
|
|
111
|
+
const paginatedResults = allResults.slice(offset, offset + limit).map((r) => r.description && r.description.length > 200
|
|
112
|
+
? { ...r, description: `${r.description.slice(0, 200)}…`, descriptionTruncated: true }
|
|
113
|
+
: r);
|
|
109
114
|
const hasMore = offset + limit < total;
|
|
110
115
|
return { results: paginatedResults, total, hasMore };
|
|
111
116
|
}
|
|
@@ -121,14 +126,14 @@ function inferCategory(name) {
|
|
|
121
126
|
*/
|
|
122
127
|
export function getCategories(manifest) {
|
|
123
128
|
const categories = new Map();
|
|
124
|
-
for (const
|
|
125
|
-
const cat = inferCategory(name);
|
|
129
|
+
for (const compSet of Object.values(manifest.componentSets)) {
|
|
130
|
+
const cat = inferCategory(compSet.name);
|
|
126
131
|
const existing = categories.get(cat) || { componentCount: 0, componentSetCount: 0 };
|
|
127
132
|
existing.componentSetCount++;
|
|
128
133
|
categories.set(cat, existing);
|
|
129
134
|
}
|
|
130
|
-
for (const
|
|
131
|
-
const cat = inferCategory(name);
|
|
135
|
+
for (const comp of Object.values(manifest.components)) {
|
|
136
|
+
const cat = inferCategory(comp.name);
|
|
132
137
|
const existing = categories.get(cat) || { componentCount: 0, componentSetCount: 0 };
|
|
133
138
|
existing.componentCount++;
|
|
134
139
|
categories.set(cat, existing);
|
|
@@ -155,35 +155,36 @@ export function extractVisualSpec(node) {
|
|
|
155
155
|
return hasData ? spec : undefined;
|
|
156
156
|
}
|
|
157
157
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
158
|
+
* Delta-encode variant visual specs against the first variant that has one.
|
|
159
|
+
* Variants in a set share most visual properties, so repeating the full spec
|
|
160
|
+
* on every variant inflates the payload ~30x for large sets. The base variant
|
|
161
|
+
* keeps its full visualSpec; every other variant gets a visualSpecDelta with
|
|
162
|
+
* only the top-level properties that differ from the base (null means the
|
|
163
|
+
* property exists on the base but not on this variant). Variants identical to
|
|
164
|
+
* the base carry neither field.
|
|
160
165
|
*/
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
for (const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
childInfo.visualSpec = childVisual;
|
|
179
|
-
if (child.characters)
|
|
180
|
-
childInfo.characters = child.characters;
|
|
181
|
-
childSpecs.push(childInfo);
|
|
166
|
+
function deltaEncodeVariantSpecs(variants) {
|
|
167
|
+
const base = variants.find((v) => v.visualSpec);
|
|
168
|
+
if (!base)
|
|
169
|
+
return;
|
|
170
|
+
const baseSpec = base.visualSpec;
|
|
171
|
+
for (const variant of variants) {
|
|
172
|
+
if (variant === base || !variant.visualSpec)
|
|
173
|
+
continue;
|
|
174
|
+
const spec = variant.visualSpec;
|
|
175
|
+
const delta = {};
|
|
176
|
+
for (const key of new Set([...Object.keys(baseSpec), ...Object.keys(spec)])) {
|
|
177
|
+
if (!(key in spec)) {
|
|
178
|
+
delta[key] = null;
|
|
179
|
+
}
|
|
180
|
+
else if (!(key in baseSpec) || JSON.stringify(spec[key]) !== JSON.stringify(baseSpec[key])) {
|
|
181
|
+
delta[key] = spec[key];
|
|
182
|
+
}
|
|
182
183
|
}
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
delete variant.visualSpec;
|
|
185
|
+
if (Object.keys(delta).length > 0)
|
|
186
|
+
variant.visualSpecDelta = delta;
|
|
185
187
|
}
|
|
186
|
-
return result;
|
|
187
188
|
}
|
|
188
189
|
/**
|
|
189
190
|
* Resolve style node IDs to their actual visual values.
|
|
@@ -428,6 +429,7 @@ export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, va
|
|
|
428
429
|
"token values per mode (light/dark), and resolved style values. " +
|
|
429
430
|
"Use this instead of calling individual tools to avoid context window overflow. " +
|
|
430
431
|
"Ideal for AI code generation — use visualSpec for pixel-accurate reproduction. " +
|
|
432
|
+
"Variant specs are delta-encoded: the base variant carries the full visualSpec, siblings carry visualSpecDelta with only the properties that differ. " +
|
|
431
433
|
"Tokens/variables are read through the connected Desktop Bridge or cloud relay and work on ANY Figma plan — no Enterprise required. " +
|
|
432
434
|
"If a tokens fetch ever reports the Variables REST API is plan-limited (403), the bridge/relay is the plan-independent path: ensure it's connected and retry rather than abandoning variables.", {
|
|
433
435
|
fileKey: z
|
|
@@ -600,6 +602,9 @@ export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, va
|
|
|
600
602
|
return entry;
|
|
601
603
|
});
|
|
602
604
|
if (variants.length > 0) {
|
|
605
|
+
// Variants share most visual properties — keep the full spec on
|
|
606
|
+
// the base variant only and encode the rest as deltas
|
|
607
|
+
deltaEncodeVariantSpecs(variants);
|
|
603
608
|
spec.variants = variants;
|
|
604
609
|
}
|
|
605
610
|
if (setNode?.componentPropertyDefinitions) {
|
|
@@ -611,11 +616,11 @@ export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, va
|
|
|
611
616
|
height: setNode.absoluteBoundingBox.height,
|
|
612
617
|
};
|
|
613
618
|
}
|
|
614
|
-
// Extract visual spec from the set node
|
|
619
|
+
// Extract visual spec from the set node itself
|
|
615
620
|
if (setNode) {
|
|
616
|
-
const
|
|
617
|
-
if (
|
|
618
|
-
spec.visualSpec =
|
|
621
|
+
const setSpec = extractVisualSpec(setNode);
|
|
622
|
+
if (setSpec) {
|
|
623
|
+
spec.visualSpec = setSpec;
|
|
619
624
|
}
|
|
620
625
|
}
|
|
621
626
|
componentSpecs.push(spec);
|
|
@@ -638,11 +643,11 @@ export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, va
|
|
|
638
643
|
height: node.absoluteBoundingBox.height,
|
|
639
644
|
};
|
|
640
645
|
}
|
|
641
|
-
// Extract visual spec from the component node
|
|
646
|
+
// Extract visual spec from the component node
|
|
642
647
|
if (node) {
|
|
643
|
-
const
|
|
644
|
-
if (
|
|
645
|
-
spec.visualSpec =
|
|
648
|
+
const nodeSpec = extractVisualSpec(node);
|
|
649
|
+
if (nodeSpec) {
|
|
650
|
+
spec.visualSpec = nodeSpec;
|
|
646
651
|
}
|
|
647
652
|
}
|
|
648
653
|
componentSpecs.push(spec);
|
|
@@ -762,6 +767,10 @@ export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, va
|
|
|
762
767
|
" - layout.itemSpacing → gap\n" +
|
|
763
768
|
" - layout.primaryAxisAlign → justify-content, counterAxisAlign → align-items\n" +
|
|
764
769
|
" - typography → font-family, font-size, font-weight, line-height, letter-spacing\n" +
|
|
770
|
+
" - Variant specs are delta-encoded: one base variant carries the full visualSpec; " +
|
|
771
|
+
"sibling variants carry 'visualSpecDelta' with ONLY the properties that differ from the base " +
|
|
772
|
+
"(null = property absent on this variant). Merge base visualSpec + visualSpecDelta to get a " +
|
|
773
|
+
"variant's full spec. A variant with neither field is visually identical to the base.\n" +
|
|
765
774
|
"3. Do NOT add decorative elements (colored borders, accents, dividers, gradients) " +
|
|
766
775
|
"unless they appear in the visualSpec data.\n" +
|
|
767
776
|
"4. Use 'imageUrl' screenshots as the visual ground truth. If the screenshot " +
|
|
@@ -34,6 +34,10 @@ function buildReport(opts) {
|
|
|
34
34
|
if (plugin.editorType && plugin.editorType !== "figma") {
|
|
35
35
|
lines.push(`- Editor type: ${plugin.editorType}.`);
|
|
36
36
|
}
|
|
37
|
+
if (plugin.pluginUpdateAvailable) {
|
|
38
|
+
const bundledVersion = opts.getBundledPluginVersion?.() ?? opts.getServerVersion();
|
|
39
|
+
lines.push(`- ⚠️ **Plugin update available**: the imported plugin${plugin.pluginVersion ? ` (v${plugin.pluginVersion})` : " (version unknown — very old)"} differs from the plugin files this server ships (v${bundledVersion}). Figma caches plugin files, so re-import it: Figma Desktop → Plugins → Development → Import plugin from manifest → select manifest.json. Until then, recently added or fixed plugin features may silently misbehave.`);
|
|
40
|
+
}
|
|
37
41
|
}
|
|
38
42
|
else {
|
|
39
43
|
lines.push(`- ⚠️ Desktop Bridge plugin not connected${plugin.port ? ` (server is listening on port ${plugin.port})` : ""}.`);
|
|
@@ -83,17 +83,23 @@ export class EnrichmentService {
|
|
|
83
83
|
const enriched = {
|
|
84
84
|
...variable,
|
|
85
85
|
};
|
|
86
|
-
// Resolve values for all modes
|
|
86
|
+
// Resolve values for all modes — each mode must resolve ITS OWN
|
|
87
|
+
// value, so the modeId is threaded through the resolver (which
|
|
88
|
+
// also keys its cache per mode).
|
|
87
89
|
if (options.include_exports !== false) {
|
|
88
90
|
const resolved_values = {};
|
|
89
|
-
for (const
|
|
90
|
-
const resolvedValue = await this.styleResolver.resolveVariableValue(variable, variablesMap, options.max_depth);
|
|
91
|
+
for (const modeId of Object.keys(variable.valuesByMode || {})) {
|
|
92
|
+
const resolvedValue = await this.styleResolver.resolveVariableValue(variable, variablesMap, options.max_depth, 0, modeId);
|
|
91
93
|
resolved_values[modeId] = resolvedValue;
|
|
92
94
|
}
|
|
93
95
|
enriched.resolved_values = resolved_values;
|
|
94
|
-
// Generate export formats using first mode
|
|
96
|
+
// Generate export formats using first mode. Guard only against
|
|
97
|
+
// null/undefined — legitimate falsy values (0, false, "") must
|
|
98
|
+
// still produce export formats.
|
|
95
99
|
const firstModeValue = Object.values(resolved_values)[0];
|
|
96
|
-
if (firstModeValue
|
|
100
|
+
if (firstModeValue !== null &&
|
|
101
|
+
firstModeValue !== undefined &&
|
|
102
|
+
options.export_formats) {
|
|
97
103
|
enriched.export_formats = this.styleResolver.generateExportFormats(variable.name, firstModeValue, variable.resolvedType, options.export_formats);
|
|
98
104
|
}
|
|
99
105
|
}
|
|
@@ -56,29 +56,38 @@ export class StyleValueResolver {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* Resolve a variable's value, handling alias chains
|
|
59
|
+
* Resolve a variable's value, handling alias chains.
|
|
60
|
+
*
|
|
61
|
+
* `modeId` selects which mode's value to resolve; omitted, it falls back
|
|
62
|
+
* to the variable's first mode (legacy behavior for callers that don't
|
|
63
|
+
* care about modes). The cache is keyed per (variable, mode) so
|
|
64
|
+
* multi-mode variables don't all resolve to the first mode's value.
|
|
60
65
|
*/
|
|
61
|
-
async resolveVariableValue(variable, allVariables, maxDepth = 10, currentDepth = 0) {
|
|
66
|
+
async resolveVariableValue(variable, allVariables, maxDepth = 10, currentDepth = 0, modeId) {
|
|
62
67
|
if (currentDepth >= maxDepth) {
|
|
63
68
|
this.logger.warn({
|
|
64
69
|
variable: variable.name,
|
|
65
70
|
}, "Max resolution depth reached");
|
|
66
71
|
return null;
|
|
67
72
|
}
|
|
68
|
-
|
|
73
|
+
// Pick the mode to resolve: the requested modeId when this variable
|
|
74
|
+
// has a value for it, otherwise the first available mode (alias
|
|
75
|
+
// targets in other collections have different modeIds).
|
|
76
|
+
const modes = Object.keys(variable.valuesByMode || {});
|
|
77
|
+
if (modes.length === 0) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const effectiveMode = modeId !== undefined && variable.valuesByMode[modeId] !== undefined
|
|
81
|
+
? modeId
|
|
82
|
+
: modes[0];
|
|
83
|
+
const cacheKey = `var:${variable.id}:${effectiveMode}`;
|
|
69
84
|
if (this.variableCache.has(cacheKey)) {
|
|
70
85
|
return this.variableCache.get(cacheKey);
|
|
71
86
|
}
|
|
72
87
|
try {
|
|
73
|
-
|
|
74
|
-
const modes = Object.keys(variable.valuesByMode || {});
|
|
75
|
-
if (modes.length === 0) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
const defaultMode = modes[0]; // TODO: Support mode selection
|
|
79
|
-
const value = variable.valuesByMode[defaultMode];
|
|
88
|
+
const value = variable.valuesByMode[effectiveMode];
|
|
80
89
|
// Check if this is an alias (reference to another variable)
|
|
81
|
-
if (typeof value === "object" && value.type === "VARIABLE_ALIAS") {
|
|
90
|
+
if (value !== null && typeof value === "object" && value.type === "VARIABLE_ALIAS") {
|
|
82
91
|
const targetVariable = allVariables.get(value.id);
|
|
83
92
|
if (!targetVariable) {
|
|
84
93
|
this.logger.warn({
|
|
@@ -87,8 +96,9 @@ export class StyleValueResolver {
|
|
|
87
96
|
}, "Variable alias target not found");
|
|
88
97
|
return null;
|
|
89
98
|
}
|
|
90
|
-
// Recursively resolve the alias
|
|
91
|
-
|
|
99
|
+
// Recursively resolve the alias, carrying the requested modeId
|
|
100
|
+
// through so same-collection targets resolve the same mode.
|
|
101
|
+
const resolvedValue = await this.resolveVariableValue(targetVariable, allVariables, maxDepth, currentDepth + 1, modeId);
|
|
92
102
|
this.variableCache.set(cacheKey, resolvedValue);
|
|
93
103
|
return resolvedValue;
|
|
94
104
|
}
|
|
@@ -109,7 +119,9 @@ export class StyleValueResolver {
|
|
|
109
119
|
* Format a variable value based on its type
|
|
110
120
|
*/
|
|
111
121
|
formatVariableValue(value, type) {
|
|
112
|
-
|
|
122
|
+
// Guard only null/undefined — 0, false, and "" are legitimate
|
|
123
|
+
// variable values (opacity 0, boolean flags off, empty strings).
|
|
124
|
+
if (value === null || value === undefined)
|
|
113
125
|
return null;
|
|
114
126
|
switch (type) {
|
|
115
127
|
case "COLOR":
|
|
@@ -133,10 +145,18 @@ export class StyleValueResolver {
|
|
|
133
145
|
return color;
|
|
134
146
|
}
|
|
135
147
|
if (color.r !== undefined && color.g !== undefined && color.b !== undefined) {
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
148
|
+
const clampByte = (f) => Math.max(0, Math.min(255, Math.round(f * 255)));
|
|
149
|
+
const toHex = (byte) => byte.toString(16).padStart(2, "0");
|
|
150
|
+
const r = clampByte(color.r);
|
|
151
|
+
const g = clampByte(color.g);
|
|
152
|
+
const b = clampByte(color.b);
|
|
153
|
+
let hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
154
|
+
// Preserve alpha: append the alpha byte for semi-transparent colors
|
|
155
|
+
// (mirrors rgbaToHex in tokens/figma-converter.ts).
|
|
156
|
+
if (color.a !== undefined && color.a < 1) {
|
|
157
|
+
hex += toHex(clampByte(color.a));
|
|
158
|
+
}
|
|
159
|
+
return hex.toUpperCase();
|
|
140
160
|
}
|
|
141
161
|
return null;
|
|
142
162
|
}
|