figrelay 0.8.1 → 0.9.0
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 +35 -216
- package/bin/darwin-amd64/figma-mcp +0 -0
- package/bin/darwin-arm64/figma-mcp +0 -0
- package/bin/linux-amd64/figma-mcp +0 -0
- package/bin/linux-arm64/figma-mcp +0 -0
- package/bin/plugin/code.js +14 -2
- package/bin/plugin/ui.html +55 -1
- package/bin/skills/figrelay-design/SKILL.md +153 -0
- package/bin/skills/figrelay-design-to-code/SKILL.md +62 -0
- package/bin/skills/figrelay-generate-design/SKILL.md +55 -0
- package/bin/skills/figrelay-generate-library/SKILL.md +124 -0
- package/bin/windows-amd64/figma-mcp.exe +0 -0
- package/bin/windows-arm64/figma-mcp.exe +0 -0
- package/package.json +1 -1
- package/run.js +18 -0
package/README.md
CHANGED
|
@@ -4,60 +4,16 @@
|
|
|
4
4
|
[](https://modelcontextprotocol.io/)
|
|
5
5
|
[](https://github.com/quannh287/figma-relay/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
> **Let your AI assistant
|
|
8
|
-
>
|
|
7
|
+
> **Let your AI assistant read and write Figma — no API token, no rate limits.**
|
|
8
|
+
> An MCP server that talks to a Figma Desktop plugin, so the AI can implement a frame in your codebase, draw a screen from a requirement, sync design tokens or bulk-edit layers. Works with any MCP client.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## 🆕 What's new since the fork
|
|
13
|
-
|
|
14
|
-
- **Prototype tools** — `set_prototype_links`, `get_prototype_links` and `set_flow_starting_point` wire and check prototype flows.
|
|
15
|
-
- **Safer `run_script`** — resending the same `token` returns the original run instead of running the script twice; timeout and disconnect errors say the script may still be writing.
|
|
16
|
-
- **Batch edits, variants, text restyling, page targeting** — fewer calls for common edits, plus a scripting escape hatch.
|
|
17
|
-
- **Many sessions, one plugin** — a timeout reports how many commands are queued, screenshots get 150s, and a busy plugin is no longer dropped as dead.
|
|
18
|
-
- **Smaller tool list** — `create_frame`/`create_rectangle`/`create_ellipse` merged into `create_shape`; hide whole groups with `FIGMA_MCP_DISABLE`.
|
|
19
|
-
- **Server version in the plugin panel**, so a stale binary is easy to spot.
|
|
20
|
-
|
|
21
|
-
**Coming from `figma-mcp-console`?** Swap the package name to `figrelay@latest`, re-import the plugin from this release, and replace calls to `create_frame`/`create_rectangle`/`create_ellipse` with `create_shape`.
|
|
22
|
-
|
|
23
|
-
## ✨ What can it do?
|
|
24
|
-
|
|
25
|
-
- 🖥️ **Figma → Code** — paste a link to a Figma frame, and the AI implements that exact screen in your codebase, exporting the design's icons and images into your project instead of inventing its own
|
|
26
|
-
- 🎨 **Requirement → Figma** — paste a requirement, and the AI draws the screen in Figma, then screenshots its own work and fixes what's off
|
|
27
|
-
- 🔁 **Token sync** — pull design tokens into your theme config, or build a design system in Figma from your code
|
|
28
|
-
- ✏️ **Bulk editing** — rename layers, replace text across a page, reorganize frames
|
|
29
|
-
|
|
30
|
-
---
|
|
10
|
+
Fork of [figma-mcp-console](https://github.com/hoangann2000/figma-mcp-console), published on npm as [`figrelay`](https://www.npmjs.com/package/figrelay); the plugin shows as **Figma Relay**. **Migrating?** Swap the package name to `figrelay@latest`, re-import the plugin from this release, and replace `create_frame`/`create_rectangle`/`create_ellipse` with `create_shape`.
|
|
31
11
|
|
|
32
12
|
## ⚡ Quick start
|
|
33
13
|
|
|
34
|
-
**
|
|
35
|
-
|
|
36
|
-
- [ ] [Node.js](https://nodejs.org) 18+ — check with `node --version`
|
|
37
|
-
- [ ] **Figma Desktop** app (the web app can't run development plugins)
|
|
14
|
+
Needs [Node.js](https://nodejs.org) 18+ and the **Figma Desktop** app (the web app cannot run development plugins).
|
|
38
15
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
No install needed — `npx` downloads and runs everything automatically.
|
|
42
|
-
|
|
43
|
-
<details>
|
|
44
|
-
<summary><b>Claude CLI</b></summary>
|
|
45
|
-
|
|
46
|
-
For one project — run inside that project (registers it there only):
|
|
47
|
-
|
|
48
|
-
```sh
|
|
49
|
-
claude mcp add figma-console -- npx -y figrelay@latest
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
For all your projects at once — add it globally instead:
|
|
53
|
-
|
|
54
|
-
```sh
|
|
55
|
-
claude mcp add --scope user figma-console -- npx -y figrelay@latest
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Both work the same at runtime: each Claude session starts its own server instance in that project's directory (so exported assets land in the right project), and all instances share one local bridge to Figma automatically.
|
|
59
|
-
|
|
60
|
-
Or add to the project's `.mcp.json`:
|
|
16
|
+
**1. Add the server to your AI tool.** Most clients take this block; [docs/mcp-client-setup.md](docs/mcp-client-setup.md) has the config-file path and the one-line CLI command for each.
|
|
61
17
|
|
|
62
18
|
```json
|
|
63
19
|
{
|
|
@@ -70,134 +26,19 @@ Or add to the project's `.mcp.json`:
|
|
|
70
26
|
}
|
|
71
27
|
```
|
|
72
28
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"mcpServers": {
|
|
83
|
-
"figma-console": {
|
|
84
|
-
"command": "npx",
|
|
85
|
-
"args": ["-y", "figrelay@latest"]
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
Restart Claude Desktop. On Windows use `"command": "cmd"`, `"args": ["/c", "npx", "-y", "figrelay@latest"]`.
|
|
92
|
-
|
|
93
|
-
</details>
|
|
94
|
-
|
|
95
|
-
<details>
|
|
96
|
-
<summary><b>Codex CLI</b></summary>
|
|
97
|
-
|
|
98
|
-
```sh
|
|
99
|
-
codex mcp add figma-console -- npx -y figrelay@latest
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Or in `~/.codex/config.toml`:
|
|
103
|
-
|
|
104
|
-
```toml
|
|
105
|
-
[mcp_servers.figma-console]
|
|
106
|
-
command = "npx"
|
|
107
|
-
args = ["-y", "figrelay@latest"]
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
</details>
|
|
111
|
-
|
|
112
|
-
<details>
|
|
113
|
-
<summary><b>VS Code (GitHub Copilot)</b></summary>
|
|
114
|
-
|
|
115
|
-
Create `.vscode/mcp.json` (or **Command Palette → "MCP: Add Server"**):
|
|
116
|
-
|
|
117
|
-
```json
|
|
118
|
-
{
|
|
119
|
-
"servers": {
|
|
120
|
-
"figma-console": {
|
|
121
|
-
"type": "stdio",
|
|
122
|
-
"command": "npx",
|
|
123
|
-
"args": ["-y", "figrelay@latest"],
|
|
124
|
-
"cwd": "${workspaceFolder}"
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Use it from Copilot Chat in **Agent** mode.
|
|
131
|
-
|
|
132
|
-
</details>
|
|
133
|
-
|
|
134
|
-
<details>
|
|
135
|
-
<summary><b>Cursor</b></summary>
|
|
136
|
-
|
|
137
|
-
Create `.cursor/mcp.json` (or **Settings → MCP → Add new MCP server**):
|
|
138
|
-
|
|
139
|
-
```json
|
|
140
|
-
{
|
|
141
|
-
"mcpServers": {
|
|
142
|
-
"figma-console": {
|
|
143
|
-
"command": "npx",
|
|
144
|
-
"args": ["-y", "figrelay@latest"]
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
</details>
|
|
151
|
-
|
|
152
|
-
<details>
|
|
153
|
-
<summary><b>Antigravity</b></summary>
|
|
154
|
-
|
|
155
|
-
Agent panel → **MCP Servers → Manage MCP servers → View raw config** (`mcp_config.json`):
|
|
156
|
-
|
|
157
|
-
```json
|
|
158
|
-
{
|
|
159
|
-
"mcpServers": {
|
|
160
|
-
"figma-console": {
|
|
161
|
-
"command": "npx",
|
|
162
|
-
"args": ["-y", "figrelay@latest"]
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Click **Refresh** after saving.
|
|
169
|
-
|
|
170
|
-
</details>
|
|
171
|
-
|
|
172
|
-
<details>
|
|
173
|
-
<summary><b>Other MCP clients (opencode, Windsurf, Cline, Gemini CLI, Zed, …)</b></summary>
|
|
174
|
-
|
|
175
|
-
Standard MCP stdio transport — any client works. Set the command to `npx` with args `["-y", "figrelay@latest"]`.
|
|
176
|
-
|
|
177
|
-
</details>
|
|
178
|
-
|
|
179
|
-
### Step 2 — Install the Figma plugin
|
|
180
|
-
|
|
181
|
-
First, get the plugin folder onto your machine — **pick either option:**
|
|
182
|
-
|
|
183
|
-
**📦 Option A — Download from GitHub (recommended)**
|
|
184
|
-
|
|
185
|
-
> 1. Download **[plugin.zip](https://github.com/quannh287/figma-relay/releases/latest)** → **unzip it**
|
|
186
|
-
|
|
187
|
-
**⌨️ Option B — Get it via command** *(if the download doesn't work)*
|
|
188
|
-
|
|
189
|
-
> 1. Run **`npx figrelay install-plugin`**
|
|
190
|
-
> 2. It saves the plugin to a folder and **opens it automatically** so you can see `manifest.json`
|
|
191
|
-
|
|
192
|
-
Then, in both cases:
|
|
193
|
-
|
|
194
|
-
2. In **Figma Desktop**: **Plugins → Development → Import plugin from manifest…** → select `manifest.json` *(one-time step)*
|
|
195
|
-
3. Run it: **Plugins → Development → Figma Relay** — and **keep the plugin window open** while you work. 🟢 Green dot = connected
|
|
196
|
-
4. Working with several Figma files at once? Run the plugin in **each** file — every window connects to the same local bridge, and the AI picks the right file per call
|
|
29
|
+
| Client | Where the config lives |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Claude CLI | `claude mcp add figma-console -- npx -y figrelay@latest`, or `.mcp.json` |
|
|
32
|
+
| Claude Desktop | Settings → Developer → Edit Config (`claude_desktop_config.json`) |
|
|
33
|
+
| Codex CLI | `codex mcp add figma-console -- npx -y figrelay@latest`, or `~/.codex/config.toml` (TOML) |
|
|
34
|
+
| VS Code (Copilot) | `.vscode/mcp.json` — **different shape**, see the doc |
|
|
35
|
+
| Cursor | `.cursor/mcp.json` |
|
|
36
|
+
| Antigravity | MCP Servers → Manage → View raw config (`mcp_config.json`) |
|
|
37
|
+
| Others | Standard MCP stdio: command `npx`, args `["-y", "figrelay@latest"]` |
|
|
197
38
|
|
|
198
|
-
|
|
39
|
+
**2. Install the Figma plugin.** Download [plugin.zip](https://github.com/quannh287/figma-relay/releases/latest) and unzip it, or run `npx figrelay@latest install-plugin` (saves it to a folder and opens it). Then in Figma Desktop: **Plugins → Development → Import plugin from manifest…** → pick `manifest.json` (one-time), and run **Plugins → Development → Figma Relay**. Keep the plugin window open — 🟢 green dot means connected. Working across several Figma files? Run the plugin in each one.
|
|
199
40
|
|
|
200
|
-
**
|
|
41
|
+
**3. Try it.** Right-click a frame → **Copy link to selection**, then ask:
|
|
201
42
|
|
|
202
43
|
```text
|
|
203
44
|
Implement this Figma page in our project: <paste link>. Match layout, spacing,
|
|
@@ -205,28 +46,22 @@ type and colors; export its icons/images into the project; screenshot the frame
|
|
|
205
46
|
and compare when done.
|
|
206
47
|
```
|
|
207
48
|
|
|
208
|
-
**
|
|
209
|
-
|
|
210
|
-
```text
|
|
211
|
-
Design a SaaS landing page in Figma (desktop, 1440px): hero + CTA, three feature
|
|
212
|
-
cards, pricing with three plans, footer. Light theme, use auto-layout, then
|
|
213
|
-
screenshot and fix anything off.
|
|
214
|
-
```
|
|
49
|
+
Or the other direction: *"Design a SaaS landing page in Figma (desktop, 1440px): hero + CTA, three feature cards, pricing, footer. Light theme, auto-layout, then screenshot and fix anything off."* Asking for a **screenshot and compare** at the end makes the AI check its own work.
|
|
215
50
|
|
|
216
|
-
|
|
51
|
+
## 🎓 Agent skills
|
|
217
52
|
|
|
218
|
-
|
|
53
|
+
The tools tell the AI *what it can call*; the skills tell it *how to do the job well* — orient before writing, auto-layout over absolute positioning, reuse components and tokens, screenshot and verify. `npx figrelay@latest install-skills` writes them to `.claude/skills/<name>/SKILL.md`; other clients take the same files in their own skills folder. Sources live in [`skills/`](skills/) — edit them to match your conventions.
|
|
219
54
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
-
|
|
55
|
+
| Skill | When it fires |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `figrelay-design` | Anything that writes to a canvas — frames, text, components, variants, variables, prototype links |
|
|
58
|
+
| `figrelay-design-to-code` | "Implement this design" — map a frame onto the repo's components and tokens, export assets |
|
|
59
|
+
| `figrelay-generate-design` | New screens built from the file's existing components and styles |
|
|
60
|
+
| `figrelay-generate-library` | Build or update a Figma design system library from the codebase, in reviewed phases |
|
|
226
61
|
|
|
227
62
|
## ✂️ Hide tools you don't use
|
|
228
63
|
|
|
229
|
-
Every listed tool
|
|
64
|
+
Every listed tool costs context, even in sessions that never touch Figma. `FIGMA_MCP_DISABLE` (or the `-disable` flag) hides whole groups: `components`, `styles`, `variables`, `prototype`. Hiding all four cuts the tool list by about a third.
|
|
230
65
|
|
|
231
66
|
```json
|
|
232
67
|
"figma-console": {
|
|
@@ -238,17 +73,7 @@ Every listed tool takes up room in the AI's context, even in sessions that never
|
|
|
238
73
|
|
|
239
74
|
## 🧠 Teach your AI what to export, and where
|
|
240
75
|
|
|
241
|
-
Exports follow the
|
|
242
|
-
|
|
243
|
-
| AI tool | Rules file |
|
|
244
|
-
|---|---|
|
|
245
|
-
| Claude CLI / Claude Desktop | `CLAUDE.md` |
|
|
246
|
-
| Codex / opencode | `AGENTS.md` |
|
|
247
|
-
| Cursor | `.cursorrules` |
|
|
248
|
-
| VS Code Copilot | `.github/copilot-instructions.md` |
|
|
249
|
-
| Windsurf | `.windsurfrules` |
|
|
250
|
-
|
|
251
|
-
> **📋 Copy-paste rules** (adjust paths — this fits Next.js):
|
|
76
|
+
Exports follow the extension the AI picks (`.svg` for vector, `.png`/`.jpg` for bitmap, optional 0.5–4x scale), so put your preferences in the project rules file once — `CLAUDE.md`, `AGENTS.md` (Codex/opencode), `.cursorrules`, `.github/copilot-instructions.md` or `.windsurfrules`:
|
|
252
77
|
|
|
253
78
|
```markdown
|
|
254
79
|
## Figma asset rules
|
|
@@ -262,19 +87,13 @@ Exports follow the file extension the AI picks (`.svg` for vector, `.png`/`.jpg`
|
|
|
262
87
|
|
|
263
88
|
| Problem | Fix |
|
|
264
89
|
|---|---|
|
|
265
|
-
| ❌ "no Figma plugin is connected" | Open the plugin
|
|
266
|
-
| 🕐 Plugin says "Searching for MCP server…" | Normal
|
|
267
|
-
| 🗂 "N Figma files are connected; pass the file parameter" | More than one plugin window is open —
|
|
268
|
-
| 🚫 The
|
|
269
|
-
| 📁 Exported files
|
|
270
|
-
| 🔤 Font errors when creating text | The
|
|
271
|
-
|
|
272
|
-
## 🛠 Contributing
|
|
273
|
-
|
|
274
|
-
Setup, how to add a tool, and the gotchas that cost time: **[CONTRIBUTING.md](CONTRIBUTING.md)**. Why the non-obvious parts work the way they do: **[docs/design-notes.md](docs/design-notes.md)**.
|
|
275
|
-
|
|
276
|
-
## 📄 License
|
|
90
|
+
| ❌ "no Figma plugin is connected" | Open the plugin (**Plugins → Development → Figma Relay**) and keep its window open — in every file the AI should touch |
|
|
91
|
+
| 🕐 Plugin says "Searching for MCP server…" | Normal until your AI client connects. If it never does, click the status line to retry, or restart the client |
|
|
92
|
+
| 🗂 "N Figma files are connected; pass the file parameter" | More than one plugin window is open — name the file you mean, or close the extras |
|
|
93
|
+
| 🚫 The server doesn't appear in your AI tool | Check Node.js 18+ (`node -v`) |
|
|
94
|
+
| 📁 Exported files land in the wrong folder | Exports are relative to where the server runs. Register it in the project-level config, or give the AI an explicit path |
|
|
95
|
+
| 🔤 Font errors when creating text | The file uses a font not installed on your machine — the error names it |
|
|
277
96
|
|
|
278
|
-
|
|
97
|
+
## 🛠 More
|
|
279
98
|
|
|
280
|
-
|
|
99
|
+
Several Figma files or projects at once: [docs/mcp-client-setup.md](docs/mcp-client-setup.md). Setup, adding a tool and the gotchas that cost time: [CONTRIBUTING.md](CONTRIBUTING.md). Why the non-obvious parts work that way: [docs/design-notes.md](docs/design-notes.md). License: [MIT](https://github.com/quannh287/figma-relay/blob/main/LICENSE) — © [Hoang Le Thien An](https://github.com/hoangann2000) (original project), fork maintained by [@quannh287](https://github.com/quannh287). Independent community project, not affiliated with or endorsed by Figma, Inc.; "Figma" is a trademark of Figma, Inc.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/bin/plugin/code.js
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
figma.showUI(__html__, { width: 280, height: 104 });
|
|
6
6
|
|
|
7
|
+
// The UI can collapse to a dot; selection changes must not resize it back.
|
|
8
|
+
let collapsed = false;
|
|
9
|
+
let fullHeight = 104;
|
|
10
|
+
|
|
7
11
|
// Tell the UI which file this plugin window runs in; the UI forwards it to
|
|
8
12
|
// the bridge on every (re)connect so calls can be routed per file when
|
|
9
13
|
// several files run the plugin at once. figma.fileKey is undefined for
|
|
@@ -1326,10 +1330,10 @@ async function postSelection() {
|
|
|
1326
1330
|
// the paste-URL prompt when a frame needs a key (64) + up to 6 visible rows
|
|
1327
1331
|
// (the list scrolls beyond that).
|
|
1328
1332
|
const rows = Math.min(items.length, 6);
|
|
1329
|
-
|
|
1333
|
+
fullHeight = items.length
|
|
1330
1334
|
? 134 + (items.length > 1 ? 22 : 0) + (needsKey ? 64 : 0) + rows * 30
|
|
1331
1335
|
: 104;
|
|
1332
|
-
figma.ui.resize(280,
|
|
1336
|
+
if (!collapsed) figma.ui.resize(280, fullHeight);
|
|
1333
1337
|
}
|
|
1334
1338
|
figma.on("selectionchange", postSelection);
|
|
1335
1339
|
postSelection();
|
|
@@ -1338,6 +1342,14 @@ postSelection();
|
|
|
1338
1342
|
|
|
1339
1343
|
figma.ui.onmessage = async (msg) => {
|
|
1340
1344
|
if (!msg) return;
|
|
1345
|
+
// Control message from the UI: collapse to a dot. The window has to stay
|
|
1346
|
+
// open for the bridge, so shrinking it is the only way to get the screen
|
|
1347
|
+
// space back. 70 is Figma's minimum width.
|
|
1348
|
+
if (typeof msg.collapsed === "boolean") {
|
|
1349
|
+
collapsed = msg.collapsed;
|
|
1350
|
+
figma.ui.resize(collapsed ? 70 : 280, collapsed ? 36 : fullHeight);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1341
1353
|
// Control message from the UI: the user pasted the file URL to enable links.
|
|
1342
1354
|
if (typeof msg.setFileKey === "string") {
|
|
1343
1355
|
setFileKey(msg.setFileKey);
|
package/bin/plugin/ui.html
CHANGED
|
@@ -57,6 +57,44 @@
|
|
|
57
57
|
box-shadow: 0 0 0 3px rgba(34, 160, 107, 0.15);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
#collapse {
|
|
61
|
+
flex: none;
|
|
62
|
+
margin-left: auto;
|
|
63
|
+
width: 18px;
|
|
64
|
+
height: 18px;
|
|
65
|
+
border: 0;
|
|
66
|
+
border-radius: 5px;
|
|
67
|
+
background: transparent;
|
|
68
|
+
color: #8288a0;
|
|
69
|
+
font-size: 13px;
|
|
70
|
+
line-height: 1;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#collapse:hover {
|
|
75
|
+
background: #eef0f7;
|
|
76
|
+
color: #2c2c34;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Collapsed: the dot only, at Figma's 70px minimum width. */
|
|
80
|
+
body.mini {
|
|
81
|
+
padding: 4px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
body.mini .card {
|
|
85
|
+
padding: 4px;
|
|
86
|
+
display: flex;
|
|
87
|
+
justify-content: center;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
body.mini #status,
|
|
91
|
+
body.mini #collapse,
|
|
92
|
+
body.mini #hint,
|
|
93
|
+
body.mini #result,
|
|
94
|
+
body.mini #credit {
|
|
95
|
+
display: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
60
98
|
#hint {
|
|
61
99
|
color: #6b6f7d;
|
|
62
100
|
margin-top: 5px;
|
|
@@ -252,6 +290,7 @@
|
|
|
252
290
|
<div class="card">
|
|
253
291
|
<div id="status-row">
|
|
254
292
|
<span id="dot"></span><span id="status">Connecting…</span>
|
|
293
|
+
<button id="collapse" title="Collapse to a dot">−</button>
|
|
255
294
|
</div>
|
|
256
295
|
<div id="hint">Keep open while AI works · run it in every file.</div>
|
|
257
296
|
<div id="result"></div>
|
|
@@ -550,7 +589,22 @@
|
|
|
550
589
|
}
|
|
551
590
|
};
|
|
552
591
|
|
|
553
|
-
|
|
592
|
+
// Collapsed, the whole card is the expand target; expanded, the status row
|
|
593
|
+
// still retries the connection.
|
|
594
|
+
let mini = false;
|
|
595
|
+
function setMini(on) {
|
|
596
|
+
mini = on;
|
|
597
|
+
document.body.classList.toggle("mini", on);
|
|
598
|
+
parent.postMessage({ pluginMessage: { collapsed: on } }, "*");
|
|
599
|
+
}
|
|
600
|
+
document.getElementById("collapse").onclick = (e) => {
|
|
601
|
+
e.stopPropagation();
|
|
602
|
+
setMini(true);
|
|
603
|
+
};
|
|
604
|
+
document.getElementById("status-row").onclick = () => {
|
|
605
|
+
if (mini) return setMini(false);
|
|
606
|
+
connect();
|
|
607
|
+
};
|
|
554
608
|
document.addEventListener("visibilitychange", () => {
|
|
555
609
|
if (!document.hidden && !ws) connect();
|
|
556
610
|
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figrelay-design
|
|
3
|
+
description: Create or modify content on a Figma Design canvas through the Figma Relay MCP server — frames, text, auto layout, components, variants, styles, variables and prototype links. Use whenever the user asks to draw, build, edit, restyle, rename, rearrange or wire anything in a Figma file. Triggers on "in Figma", "add a frame/button/screen", "change this component", "set up tokens/variables", "wire the prototype".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing to a Figma file
|
|
7
|
+
|
|
8
|
+
The plugin edits **the page that is open in Figma**, which the user can change at
|
|
9
|
+
any moment. Never assume where your work lands.
|
|
10
|
+
|
|
11
|
+
## 1. Orient before writing
|
|
12
|
+
|
|
13
|
+
- `list_files` — only when several Figma files are connected; pass `file` to every
|
|
14
|
+
later call.
|
|
15
|
+
- `list_pages` then `set_current_page` — put the right page in front before creating.
|
|
16
|
+
- `get_metadata` for the page's top-level layers, `get_selection` for what the user
|
|
17
|
+
is pointing at. In a large file use `find_nodes` (by name, text, type, fill, font)
|
|
18
|
+
instead of walking the tree with `get_design_context`.
|
|
19
|
+
- `get_design_context` with `compact: true`. The full form costs several thousand
|
|
20
|
+
tokens for one frame; only drop `compact` when you need raw paint objects.
|
|
21
|
+
|
|
22
|
+
## 2. Build top-down
|
|
23
|
+
|
|
24
|
+
1. A root `create_shape` FRAME at an empty spot, sized for the target
|
|
25
|
+
(390x844 mobile, 1440x900 desktop). Everything else goes inside it via `parent_id`.
|
|
26
|
+
2. `set_auto_layout` on every container — direction, `item_spacing`, padding,
|
|
27
|
+
sizing modes. Only position with x/y when auto layout genuinely does not fit:
|
|
28
|
+
absolute children do not reflow when content changes.
|
|
29
|
+
3. `create_text` for copy, `create_shape` / `create_line` for structure.
|
|
30
|
+
Name every layer the way a designer would ("Header", "CTA / Primary"), never
|
|
31
|
+
"Rectangle 12".
|
|
32
|
+
4. Style: `set_fills`, `set_strokes`, `set_corner_radius`, `set_effects`,
|
|
33
|
+
`set_text_properties`.
|
|
34
|
+
|
|
35
|
+
Reuse before you create: `get_local_components` + `create_instance`,
|
|
36
|
+
`get_local_styles` + `apply_style`, `get_variable_defs` + `set_bound_variable`.
|
|
37
|
+
A hard-coded hex where a token exists is a bug.
|
|
38
|
+
|
|
39
|
+
## 3. Verify visually
|
|
40
|
+
|
|
41
|
+
`get_screenshot` on the root frame after each meaningful chunk — not once at the
|
|
42
|
+
end. Compare against the brief, fix with `move_nodes`, `resize_nodes`,
|
|
43
|
+
`set_characters`, `set_fills`, `remove_nodes`, screenshot again. A tall node comes
|
|
44
|
+
back scaled with a warning; screenshot a child instead of reading a thumbnail.
|
|
45
|
+
|
|
46
|
+
Finish with `set_selection` on the root frame so the user sees the result.
|
|
47
|
+
|
|
48
|
+
## 4. Components, variables, prototypes
|
|
49
|
+
|
|
50
|
+
- **Component**: build the frame, then `create_component`. For variants, name each
|
|
51
|
+
component `Property=Value` (`Size=Large`) *before* `combine_as_variants`.
|
|
52
|
+
`add_component_property` returns the key `set_instance_properties` needs.
|
|
53
|
+
- **Variables**: `create_variable_collection` → `add_variable_mode` (light/dark) →
|
|
54
|
+
`create_variable` → `set_variable_value` per mode → `set_bound_variable` on nodes.
|
|
55
|
+
These are for single edits; a whole token set goes through `run_script`
|
|
56
|
+
(see below). **Check the mode limit before designing around modes**: on a
|
|
57
|
+
free/starter plan `add_variable_mode` fails with *"Limited to 1 modes only"* and
|
|
58
|
+
the light/dark-via-modes pattern is simply unavailable — the fallback is two
|
|
59
|
+
sibling collections (`tokens · Light`, `tokens · Dark`) and no mode switching.
|
|
60
|
+
Name variables with slashes (`primary/500`, not `color-primary-500`) — that is
|
|
61
|
+
what groups them in the Variables panel; flat names are unusable past ~50 tokens.
|
|
62
|
+
- **Prototype**: `set_prototype_links` only joins **top-level frames on the same
|
|
63
|
+
page**; a nested frame is rejected. Each call replaces that node's reactions, so
|
|
64
|
+
pass all of a node's links together. Check with `get_prototype_links`,
|
|
65
|
+
mark the entry with `set_flow_starting_point`.
|
|
66
|
+
|
|
67
|
+
## Gotchas that cost time
|
|
68
|
+
|
|
69
|
+
- **Fonts**: `create_text` fails if the family is not installed locally.
|
|
70
|
+
`list_available_fonts` first when using anything but the file's existing fonts.
|
|
71
|
+
- **Rename, don't rebuild**: `rename_nodes`. Deleting and recreating a component
|
|
72
|
+
breaks every existing instance.
|
|
73
|
+
- **`resize_nodes` resets auto-layout sizing modes to FIXED.** Resize *before*
|
|
74
|
+
`set_auto_layout`, or re-apply the sizing modes afterwards — otherwise a frame
|
|
75
|
+
set to hug ends up locked at the resized height with its content spilling out.
|
|
76
|
+
It also does not move children unless they have constraints or auto layout.
|
|
77
|
+
- **Instances**: children have derived ids `I<instance-id>;<child-id>` — accepted
|
|
78
|
+
by every tool, returned by `find_nodes`.
|
|
79
|
+
- **Batch**: most edit tools take `node_ids`; feed `find_nodes` output straight in.
|
|
80
|
+
|
|
81
|
+
## `run_script` — the bulk path
|
|
82
|
+
|
|
83
|
+
Default for anything token-shaped (importing 150 variables as 150 calls is absurd)
|
|
84
|
+
and for bulk rebinding. Single edits stay on the atomic tools: they fail with a
|
|
85
|
+
precise error, while a half-failed script leaves the document half-changed.
|
|
86
|
+
|
|
87
|
+
- **A thrown script keeps everything it already created.** Write find-or-create
|
|
88
|
+
scripts keyed by name so a retry adopts the orphan instead of duplicating it.
|
|
89
|
+
- **Read the line number before retrying.** The throw is often on the *return*
|
|
90
|
+
line — e.g. `return { n: figma.getLocalTextStyles().length }` — meaning the work
|
|
91
|
+
completed and only the summary failed. Inspect the document, then decide.
|
|
92
|
+
- **dynamic-page access**: sync accessors throw. Async-only —
|
|
93
|
+
`getLocalVariableCollectionsAsync`, `getVariableByIdAsync`,
|
|
94
|
+
`getLocalTextStylesAsync`, `getLocalEffectStylesAsync`, `getNodeByIdAsync`,
|
|
95
|
+
`setCurrentPageAsync`, `loadAllPagesAsync`, `node.getMainComponentAsync`,
|
|
96
|
+
`setTextStyleIdAsync`, `setEffectStyleIdAsync`, `setReactionsAsync`.
|
|
97
|
+
Still sync: `variables.createVariable`, `createVariableCollection`,
|
|
98
|
+
`setValueForMode`, `valuesByMode`, `createTextStyle`, `createEffectStyle`,
|
|
99
|
+
`combineAsVariants`.
|
|
100
|
+
- **Never call an async-only getter inside a `findAll`/`filter` predicate** — it
|
|
101
|
+
crashes the whole traversal (*"findAll callback crashed"*). Collect the nodes
|
|
102
|
+
first, then `for...of` with `await`.
|
|
103
|
+
- **Seed a bound paint from the variable's own value** — free insurance, not a
|
|
104
|
+
cure. Figma normalises the stored colour to the variable's resolved value at
|
|
105
|
+
bind time, so an in-file binding seeded black still renders correctly
|
|
106
|
+
(measured). The seed only ever matters where the variable *cannot* resolve —
|
|
107
|
+
a cross-file or library binding — which is untested. Do it anyway; it costs
|
|
108
|
+
one property read.
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
const col = (await figma.variables.getLocalVariableCollectionsAsync()).find(c => c.name === 'tokens · Light');
|
|
112
|
+
const mode = col.modes[0].modeId;
|
|
113
|
+
const v = await figma.variables.getVariableByIdAsync(id);
|
|
114
|
+
const paint = { type: 'SOLID', color: v.valuesByMode[mode], opacity: 1 };
|
|
115
|
+
node.fills = [figma.variables.setBoundVariableForPaint(paint, 'color', v)];
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Rebinding and deleting variables
|
|
119
|
+
|
|
120
|
+
Measured, not guessed:
|
|
121
|
+
|
|
122
|
+
- **Rebinding is safe and needs no reseed.** `setBoundVariableForPaint(paint, 'color', newVar)`
|
|
123
|
+
with the stale colour lands on the new variable's value. Mutating
|
|
124
|
+
`paint.boundVariables.color = {type:'VARIABLE_ALIAS', id}` directly works too.
|
|
125
|
+
- **Renaming never breaks a binding.** Bindings are by id; a renamed collection
|
|
126
|
+
or variable keeps every consumer. Rename freely.
|
|
127
|
+
- **Deleting a collection leaves silent zombies.** Nodes still bound keep the
|
|
128
|
+
alias and the last literal colour — they look right and are dead. Worse,
|
|
129
|
+
`getVariableByIdAsync(deadId)` and `getVariableCollectionByIdAsync` **still
|
|
130
|
+
return live-looking objects with correct names**, so a null check detects
|
|
131
|
+
nothing. The only reliable test is list membership:
|
|
132
|
+
`!(await figma.variables.getLocalVariablesAsync()).some(v => v.id === aliasId)`.
|
|
133
|
+
- **There is no consumer API.** `variable.consumers` is `undefined`, and
|
|
134
|
+
`Object.keys()` on a variable or collection returns `["id"]` — they are
|
|
135
|
+
proxies, so don't introspect them. To find consumers, scan the tree via
|
|
136
|
+
`node.boundVariables?.fills?.some(a => ids.includes(a.id))` (cheaper than
|
|
137
|
+
digging through each paint's `boundVariables`). A plain property read like
|
|
138
|
+
this is fine inside a `findAll` predicate; an async getter is not.
|
|
139
|
+
- **`collection.remove()` is immediate with no API undo.** Re-scan and assert
|
|
140
|
+
zero remaining consumers *before* removing — you get no error if you miss some.
|
|
141
|
+
|
|
142
|
+
- Split by **logical group**, not node count — the point is error isolation.
|
|
143
|
+
Scripts creating 300+ variables, or several hundred nodes, run without timing out.
|
|
144
|
+
- Pass a `token` anyway (it costs nothing). A timed-out script **keeps running and
|
|
145
|
+
keeps writing**, so the token is what lets a retry join the original run instead
|
|
146
|
+
of duplicating it. Never redo a timed-out script's work blind.
|
|
147
|
+
|
|
148
|
+
Raw-API-only gotchas (the atomic tools already handle these):
|
|
149
|
+
`figma.combineAsVariants` neither names nor lays out the set — set `name`,
|
|
150
|
+
`layoutMode`, `layoutWrap`, `primaryAxisSizingMode = 'FIXED'`, `resize`, padding
|
|
151
|
+
and spacing yourself, or every variant stacks at 0,0. `layoutAlign = 'STRETCH'`
|
|
152
|
+
does not stretch a child unless `primaryAxisSizingMode` is also `FIXED`.
|
|
153
|
+
`layoutPositioning = 'ABSOLUTE'` must be set *after* `appendChild`.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figrelay-design-to-code
|
|
3
|
+
description: Turn a Figma frame, component or selection into code in this repository — read the design through the Figma Relay MCP server, map it onto existing components and design tokens, export the assets it needs. Use when the user says "implement this design", "build this screen", "code this component", "match the Figma", or pastes/points at a Figma selection.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Figma → code
|
|
7
|
+
|
|
8
|
+
Goal: code that matches the design **using this codebase's own components and
|
|
9
|
+
tokens**. A pixel-perfect one-off built from raw divs and hex codes is a failure.
|
|
10
|
+
|
|
11
|
+
## 1. Read the design
|
|
12
|
+
|
|
13
|
+
- `get_selection` when the user says "this" / "the selected frame"; otherwise
|
|
14
|
+
`find_nodes` by layer name. `get_metadata` / `list_pages` if you are lost.
|
|
15
|
+
- `get_design_context` with `compact: true` and a sensible `depth` (2–3 first,
|
|
16
|
+
deeper only where needed). Full depth on a whole screen buries the real
|
|
17
|
+
structure in thousands of tokens.
|
|
18
|
+
- `get_screenshot` on the frame. Read the picture and the tree together — the
|
|
19
|
+
tree gives names and numbers, the picture gives intent.
|
|
20
|
+
|
|
21
|
+
## 2. Map before you write
|
|
22
|
+
|
|
23
|
+
- `get_variable_defs` and `get_local_styles` → the design's tokens. Match them to
|
|
24
|
+
the repo's theme/tokens by **value and name**; write the repo's token, never the
|
|
25
|
+
raw hex. Report any design token that has no counterpart instead of inventing one.
|
|
26
|
+
Read the repo's token **source** (theme config, tokens JSON, Tailwind config) —
|
|
27
|
+
never a built CSS bundle, which is utility output and gets truncated before the
|
|
28
|
+
`:root` block.
|
|
29
|
+
- `get_local_components` → named components in the design. Grep this repo for the
|
|
30
|
+
same concept (Button, Card, Modal) and reuse it. Only build new when nothing fits.
|
|
31
|
+
- Auto layout maps onto flex: `direction` → `flex-direction`, `item_spacing` →
|
|
32
|
+
`gap`, padding → padding, sizing modes → `fixed` / `hug` (fit-content) /
|
|
33
|
+
`fill` (flex: 1). Absolutely positioned children are the exception, not the rule.
|
|
34
|
+
- Fixed pixel widths in the design usually mean "fill the container" in code.
|
|
35
|
+
Ask what the responsive behaviour is rather than hard-coding 390px.
|
|
36
|
+
|
|
37
|
+
## 3. Export assets
|
|
38
|
+
|
|
39
|
+
`download_assets`, one item per node, chosen per node after checking its type and
|
|
40
|
+
fills:
|
|
41
|
+
|
|
42
|
+
- **SVG** for pure vector — icons, logos, path illustrations.
|
|
43
|
+
- **PNG at `scale: 2`** for anything with a raster IMAGE fill, a photo, or
|
|
44
|
+
blur/shadow effects SVG cannot represent.
|
|
45
|
+
- **JPG** only for large photos where size matters.
|
|
46
|
+
|
|
47
|
+
Paths follow this project's conventions (check `CLAUDE.md` / `AGENTS.md` and
|
|
48
|
+
where existing assets live). Without a convention, defaults are
|
|
49
|
+
`assets/icons/<name>.svg` and `assets/images/<name>.<ext>`. Everything is written
|
|
50
|
+
relative to the directory the MCP server runs in and cannot escape it.
|
|
51
|
+
|
|
52
|
+
## 4. Verify
|
|
53
|
+
|
|
54
|
+
Run the app or the component's story, then compare with `get_screenshot` of the
|
|
55
|
+
Figma node side by side. Spacing, font size/weight, radius and colour are the
|
|
56
|
+
usual misses. State plainly what you could not match and why.
|
|
57
|
+
|
|
58
|
+
## Don't
|
|
59
|
+
|
|
60
|
+
- Don't copy absolute x/y into CSS. Rebuild with flex from the auto layout.
|
|
61
|
+
- Don't invent token names. Use the repo's, or flag the gap.
|
|
62
|
+
- Don't export a whole frame as one PNG to "save time" — export its real assets.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figrelay-generate-design
|
|
3
|
+
description: Assemble full screens in Figma out of the file's existing components, styles and variables through the Figma Relay MCP server — instead of drawing rectangles from scratch. Use when the user asks for a new page/screen/flow "using our design system", "with our components", or wants several screens that stay consistent.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Generating screens from a design system
|
|
7
|
+
|
|
8
|
+
Rule: **instantiate, don't draw.** A screen made of raw rectangles is not a
|
|
9
|
+
deliverable when the file has components.
|
|
10
|
+
|
|
11
|
+
## 1. Inventory first (do not skip)
|
|
12
|
+
|
|
13
|
+
- `get_local_components` — every COMPONENT / COMPONENT_SET in the file, all pages.
|
|
14
|
+
- `get_local_styles` — paint, text, effect styles.
|
|
15
|
+
- `get_variable_defs` — collections, modes, tokens.
|
|
16
|
+
- `get_metadata` + `list_pages` — where existing screens live; open the right page
|
|
17
|
+
with `set_current_page`.
|
|
18
|
+
|
|
19
|
+
Screenshot one existing screen (`find_nodes` → `get_screenshot`) to learn the
|
|
20
|
+
house layout: page padding, section spacing, header/footer pattern. Copy it.
|
|
21
|
+
|
|
22
|
+
Report what is missing before building. If there is no Card component, say so and
|
|
23
|
+
ask whether to build one or approximate — don't silently fake it.
|
|
24
|
+
|
|
25
|
+
## 2. Compose
|
|
26
|
+
|
|
27
|
+
1. Root `create_shape` FRAME at the target size, named like the existing screens,
|
|
28
|
+
placed to the right of them with a clear gap.
|
|
29
|
+
2. Resize the root first, **then** `set_auto_layout` VERTICAL on it with the house
|
|
30
|
+
padding and spacing — resizing afterwards resets sizing modes to FIXED.
|
|
31
|
+
Every section inside is itself an auto-layout frame. Nothing absolute.
|
|
32
|
+
3. `create_instance` for each component, `set_instance_properties` for its variant
|
|
33
|
+
and text overrides. Swap with `swap_component` rather than detaching.
|
|
34
|
+
**Never `detach_instance`** to "make it fit" — that is how a design system dies.
|
|
35
|
+
4. Only `create_text` / `create_shape` for genuinely bespoke content, and then
|
|
36
|
+
style it with `apply_style` / `set_bound_variable`, never a literal hex.
|
|
37
|
+
|
|
38
|
+
## 3. Verify and iterate
|
|
39
|
+
|
|
40
|
+
`get_screenshot` per section as you go, not once at the end. Check against the
|
|
41
|
+
house screen: alignment, rhythm, and that every colour came from a style or
|
|
42
|
+
variable. Fix, re-screenshot, then `set_selection` on the root frame.
|
|
43
|
+
|
|
44
|
+
## Multiple screens
|
|
45
|
+
|
|
46
|
+
Build the first screen, screenshot it, **get the user's sign-off**, then clone
|
|
47
|
+
the structure for the rest (`clone_node` with `parent_id` and x/y — a clone inside
|
|
48
|
+
a SECTION otherwise lands at 0,0). Wire the flow with `set_prototype_links`
|
|
49
|
+
between the top-level frames and `set_flow_starting_point` on the entry screen.
|
|
50
|
+
|
|
51
|
+
## Bulk work
|
|
52
|
+
|
|
53
|
+
Dozens of identical instances, or rebinding every fill in a subtree, is what
|
|
54
|
+
`run_script` is for. Everything smaller stays on the atomic tools — they fail with
|
|
55
|
+
a precise error instead of half-writing the document.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figrelay-generate-library
|
|
3
|
+
description: Build or update a Figma design system library from this codebase through the Figma Relay MCP server — tokens as variables, text/paint/effect styles, then components with variants. Runs in phases and stops for review between them. Use when the user asks to "create our design system in Figma", "sync our tokens to Figma", or "turn our component library into Figma components".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Codebase → Figma library
|
|
7
|
+
|
|
8
|
+
Long, destructive-by-accident work. **Stop for review after every phase.** Never
|
|
9
|
+
run the whole thing in one go.
|
|
10
|
+
|
|
11
|
+
## Phase 1 — Discovery (read only)
|
|
12
|
+
|
|
13
|
+
- In the repo: find the **token source file** (theme config, design-tokens JSON,
|
|
14
|
+
Tailwind config, CSS custom properties in source form) and the per-component
|
|
15
|
+
style definitions (`tva({...})`, variant maps, style objects). Grep, don't guess.
|
|
16
|
+
- In Figma: `get_variable_defs`, `get_local_styles`, `get_local_components`,
|
|
17
|
+
`list_pages` — what already exists.
|
|
18
|
+
- **Check the plan's mode limit** before promising light/dark via modes — see
|
|
19
|
+
Phase 2.
|
|
20
|
+
|
|
21
|
+
**Never reverse-engineer a compiled stylesheet.** A built CSS bundle is hundreds
|
|
22
|
+
of KB of utility output, gets truncated by the read cap, and usually hides the
|
|
23
|
+
`:root` block past the cut. The config source has the literal values, and the
|
|
24
|
+
component's variant block has the real spec — `xs: 'px-3.5 h-8'` gives you
|
|
25
|
+
padding 14 / height 32 and the exact token name `bg-primary-500`. That fidelity
|
|
26
|
+
is what makes the library match the app; screenshots and built CSS do not.
|
|
27
|
+
|
|
28
|
+
Report a plan: tokens found, components found, what already exists in Figma, what
|
|
29
|
+
you propose to create vs update, and anything ambiguous. **Wait for approval.**
|
|
30
|
+
|
|
31
|
+
## Phase 2 — Foundations (variables and styles)
|
|
32
|
+
|
|
33
|
+
Do this phase in `run_script`, not the typed variable tools — a hundred tokens is
|
|
34
|
+
a hundred round-trips otherwise. Read the `run_script` rules in
|
|
35
|
+
`figrelay-design` first; the ones that bite hardest here:
|
|
36
|
+
|
|
37
|
+
- **Modes are a plan limit.** `addMode` on a free/starter plan throws
|
|
38
|
+
*"Limited to 1 modes only"*. Probe it on the first collection before building
|
|
39
|
+
anything around light/dark. Fallback: two sibling collections
|
|
40
|
+
(`tokens · Light`, `tokens · Dark`), components bound to Light, and tell the
|
|
41
|
+
user plainly that mode switching is off the table. Discovering this 300
|
|
42
|
+
variables in means redoing the phase.
|
|
43
|
+
- **The script that throws keeps what it made.** The collection created on the
|
|
44
|
+
line before a failed `addMode` survives. Every script here is find-or-create
|
|
45
|
+
keyed by name, and adopts an orphan rather than making a second one:
|
|
46
|
+
`let c = cols.find(x => x.name === target) ?? adopt(cols.find(x => x.name === 'tokens'))`.
|
|
47
|
+
- **Slash names** (`primary/500`, `space/md`) — that is what makes the groups in
|
|
48
|
+
the Variables panel. Convert `--color-primary-500` on the way in.
|
|
49
|
+
- **Seed bound paints from `valuesByMode`** — cheap insurance. In-file bindings
|
|
50
|
+
render correctly even seeded black (Figma normalises at bind time); the seed
|
|
51
|
+
only matters for bindings that cannot resolve.
|
|
52
|
+
- Names mirror the code so a human can trace a token back to its source.
|
|
53
|
+
- Updating an existing collection: set the value on the existing variable.
|
|
54
|
+
A duplicate collection is the failure mode here.
|
|
55
|
+
- `create_paint_style`, `create_text_style`, `create_effect_style` for what
|
|
56
|
+
designers pick from the UI.
|
|
57
|
+
|
|
58
|
+
**Stop and show `get_variable_defs` output.**
|
|
59
|
+
|
|
60
|
+
### Splitting or merging collections
|
|
61
|
+
|
|
62
|
+
Needed whenever the mode limit forces one collection into two. Every step below
|
|
63
|
+
is measured; the sequence as a bulk operation is not, so verify as you go.
|
|
64
|
+
|
|
65
|
+
1. Create the target collection's variables with the same names, copying values
|
|
66
|
+
from the source via `valuesByMode`.
|
|
67
|
+
2. Build a `name → newVariable` map.
|
|
68
|
+
3. Scan for consumers with `node.boundVariables?.fills?.some(a => ids.includes(a.id))`
|
|
69
|
+
and rebind each hit. Rebinding preserves the rendered value and needs no
|
|
70
|
+
reseed; renaming a collection or variable never breaks a binding.
|
|
71
|
+
4. **Re-scan and assert zero consumers of the old collection before removing it.**
|
|
72
|
+
This is the whole safety net: a node left bound to a removed collection keeps
|
|
73
|
+
its alias and its last colour, looks perfectly fine, and is dead — and
|
|
74
|
+
`getVariableByIdAsync` still returns a live-looking object for it, so a null
|
|
75
|
+
check finds nothing. Test with list membership
|
|
76
|
+
(`getLocalVariablesAsync().some(v => v.id === aliasId)`), never fetchability.
|
|
77
|
+
5. Only then `collection.remove()` — immediate, with no undo from the API.
|
|
78
|
+
|
|
79
|
+
## Phase 3 — File structure
|
|
80
|
+
|
|
81
|
+
One page per group (Foundations, Components, Patterns) via the user's convention;
|
|
82
|
+
`set_current_page` before building. Moving finished components to another page
|
|
83
|
+
later is safe — cross-page instance references hold (verified over 54 instances);
|
|
84
|
+
`set_current_page` in a script is `setCurrentPageAsync`. A labelled section per component, laid out on
|
|
85
|
+
a grid with generous spacing — a library people scroll through, not a pile.
|
|
86
|
+
|
|
87
|
+
## Phase 4 — Components
|
|
88
|
+
|
|
89
|
+
Per component, simplest first:
|
|
90
|
+
|
|
91
|
+
1. Build the base frame, **resize it first, then `set_auto_layout`** — resizing
|
|
92
|
+
after configuring sizing modes silently resets them to FIXED and you get a
|
|
93
|
+
component locked at the resized height with its text spilling out. Bind every
|
|
94
|
+
colour/radius/space to the Phase 2 variables (`set_bound_variable`) or styles
|
|
95
|
+
(`apply_style`).
|
|
96
|
+
2. Build each variant as its own frame named `Property=Value`
|
|
97
|
+
(`Variant=Primary`, `Size=Large`), `create_component` each, then
|
|
98
|
+
`combine_as_variants`.
|
|
99
|
+
3. `add_component_property` for TEXT / BOOLEAN / INSTANCE_SWAP props, bound to the
|
|
100
|
+
child layer they drive, so the property actually does something.
|
|
101
|
+
4. `get_screenshot` the set and compare against the real rendered component.
|
|
102
|
+
|
|
103
|
+
Batch by component family, pause between families.
|
|
104
|
+
|
|
105
|
+
## Phase 5 — QA
|
|
106
|
+
|
|
107
|
+
- Every component: screenshot vs the real thing in the app.
|
|
108
|
+
- Every fill/radius/spacing: bound to a variable or style, no stray hex
|
|
109
|
+
(`find_nodes` by fill colour catches leftovers).
|
|
110
|
+
- Naming matches the code, variants resolve, instances still work after edits.
|
|
111
|
+
- Report coverage: what shipped, what was skipped, what needs a designer.
|
|
112
|
+
|
|
113
|
+
## Never
|
|
114
|
+
|
|
115
|
+
- Never `remove_nodes` on existing library components to "rebuild cleanly" —
|
|
116
|
+
it breaks every instance across every file. `rename_nodes` and edit in place.
|
|
117
|
+
- Never run a phase the user has not approved.
|
|
118
|
+
- Never resize a frame after setting its auto-layout sizing modes.
|
|
119
|
+
- Never treat a `run_script` error as "nothing happened" — check the document
|
|
120
|
+
first; the throw is often on the return line, after all the work succeeded.
|
|
121
|
+
- Never design around variable modes without confirming the plan allows >1.
|
|
122
|
+
- Never scrape compiled CSS for tokens when the config source is in the repo.
|
|
123
|
+
- Never remove a variable collection without first asserting it has no
|
|
124
|
+
consumers left — the failure is silent and looks correct.
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -51,6 +51,24 @@ if (process.argv[2] === "install-plugin") {
|
|
|
51
51
|
process.exit(0);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Subcommand: install-skills — copy the agent skills into the current project
|
|
55
|
+
// so the AI client picks them up from .claude/skills/<name>/SKILL.md.
|
|
56
|
+
if (process.argv[2] === "install-skills") {
|
|
57
|
+
const srcDir = path.join(__dirname, "bin", "skills");
|
|
58
|
+
const destDir = path.join(process.cwd(), ".claude", "skills");
|
|
59
|
+
if (!fs.existsSync(srcDir)) fail("skills not found in this package");
|
|
60
|
+
for (const name of fs.readdirSync(srcDir)) {
|
|
61
|
+
fs.mkdirSync(path.join(destDir, name), { recursive: true });
|
|
62
|
+
fs.copyFileSync(
|
|
63
|
+
path.join(srcDir, name, "SKILL.md"),
|
|
64
|
+
path.join(destDir, name, "SKILL.md"),
|
|
65
|
+
);
|
|
66
|
+
process.stdout.write(` ${name}\n`);
|
|
67
|
+
}
|
|
68
|
+
process.stdout.write(`\n✅ Skills installed into ${destDir}\n`);
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
54
72
|
if (!fs.existsSync(binPath)) fail(`binary not found for ${plat}-${arch}`);
|
|
55
73
|
try {
|
|
56
74
|
fs.chmodSync(binPath, 0o755);
|