figmatk 0.0.6 → 0.0.8
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +36 -253
- package/lib/api.mjs +75 -20
- package/package.json +1 -1
- package/skills/figmatk/SKILL.md +192 -27
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figmatk",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "Create and edit Figma Slides .deck files programmatically — no Figma API required",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "FigmaTK Contributors"
|
|
7
7
|
},
|
package/README.md
CHANGED
|
@@ -1,37 +1,34 @@
|
|
|
1
1
|
# FigmaTK — Figma Toolkit (v0.0.6)
|
|
2
2
|
|
|
3
|
-
Swiss-army knife CLI for Figma `.deck`
|
|
3
|
+
Swiss-army knife CLI for Figma Slides `.deck` files. Parse, inspect, modify, and rebuild presentations programmatically — no Figma API required.
|
|
4
4
|
|
|
5
5
|
## Figma File Formats
|
|
6
6
|
|
|
7
7
|
Each Figma product has its own native file format:
|
|
8
8
|
|
|
9
|
-
| Product | Extension |
|
|
10
|
-
|
|
11
|
-
| Figma
|
|
12
|
-
| Figma
|
|
13
|
-
| Figma Jam (whiteboard) | `.jam` |
|
|
14
|
-
| Figma Buzz | `.buzz` |
|
|
15
|
-
| Figma Sites | `.site` |
|
|
16
|
-
| Figma Make | `.make` |
|
|
17
|
-
|
|
18
|
-
The `.deck` format borrows heavily from `.fig` — a `.deck` is essentially a ZIP containing a `.fig`-encoded canvas plus metadata and images. FigmaTK focuses on `.deck` files. We'll evaluate expanding to other formats as we go.
|
|
9
|
+
| Product | Extension | Supported |
|
|
10
|
+
|---------|-----------|-----------|
|
|
11
|
+
| Figma Slides | `.deck` | ✅ |
|
|
12
|
+
| Figma Design | `.fig` | ❌ not yet |
|
|
13
|
+
| Figma Jam (whiteboard) | `.jam` | ❌ not yet |
|
|
14
|
+
| Figma Buzz | `.buzz` | ❌ not yet |
|
|
15
|
+
| Figma Sites | `.site` | ❌ not yet |
|
|
16
|
+
| Figma Make | `.make` | ❌ not yet |
|
|
19
17
|
|
|
20
18
|
## Why native `.deck`?
|
|
21
19
|
|
|
22
|
-
Figma Slides lets you download presentations as `.deck` files
|
|
20
|
+
Figma Slides lets you download presentations as `.deck` files and re-upload them. This is the **native** round-trip format. Exporting to `.pptx` is lossy — vectors get rasterized, fonts fall back to system defaults, layout breaks. By staying in `.deck`, you preserve everything exactly as Figma renders it.
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
FigmaTK makes this round-trip programmable. Download a `.deck`, modify it, re-upload. Everything stays native.
|
|
25
23
|
|
|
26
|
-
Plug in [Claude Code](https://claude.ai/code)
|
|
24
|
+
Plug in [Claude Code](https://claude.ai/code) or any coding agent and you have an AI that can read and edit Figma presentations end-to-end — without ever opening the Figma UI.
|
|
27
25
|
|
|
28
26
|
## Use Cases
|
|
29
27
|
|
|
30
|
-
- **AI agent for presentations** — let an LLM
|
|
31
|
-
- **Batch-produce branded decks** — start from a
|
|
32
|
-
- **Inspect and audit** — understand the internal structure of any `.deck`
|
|
28
|
+
- **AI agent for presentations** — let an LLM rewrite copy, insert images, and produce a ready-to-upload `.deck`
|
|
29
|
+
- **Batch-produce branded decks** — start from a template, feed in data per client/project, get pixel-perfect slides out
|
|
30
|
+
- **Inspect and audit** — understand the internal structure of any `.deck` file
|
|
33
31
|
- **Automate** text and image placement across dozens of slides in seconds
|
|
34
|
-
- **Validate** your pipeline with lossless roundtrip encode/decode
|
|
35
32
|
|
|
36
33
|
## Install
|
|
37
34
|
|
|
@@ -39,269 +36,55 @@ Plug in [Claude Code](https://claude.ai/code), [Codex](https://openai.com/index/
|
|
|
39
36
|
npm install -g figmatk
|
|
40
37
|
```
|
|
41
38
|
|
|
42
|
-
No build step. Pure ESM
|
|
39
|
+
Node 18+. No build step. Pure ESM.
|
|
43
40
|
|
|
44
41
|
## Quick Start
|
|
45
42
|
|
|
46
43
|
```bash
|
|
47
|
-
|
|
48
|
-
figmatk
|
|
49
|
-
|
|
50
|
-
# List every text field and image in every slide
|
|
51
|
-
figmatk list-text my-presentation.deck
|
|
52
|
-
|
|
53
|
-
# Discover all override keys (what you can edit)
|
|
54
|
-
figmatk list-overrides my-presentation.deck
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## Commands
|
|
58
|
-
|
|
59
|
-
### `inspect` — Document structure
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
figmatk inspect file.deck [--depth N] [--type TYPE] [--json]
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Prints the full node hierarchy tree:
|
|
66
|
-
|
|
67
|
-
```
|
|
68
|
-
Nodes: 1314 Slides: 10 active / 10 total Blobs: 465
|
|
69
|
-
|
|
70
|
-
DOCUMENT "Document" (0:0)
|
|
71
|
-
CANVAS "Page 1" (0:1)
|
|
72
|
-
SLIDE_GRID "Presentation" (0:3)
|
|
73
|
-
SLIDE_ROW "Row" (1:1563)
|
|
74
|
-
SLIDE "1" (1:1559)
|
|
75
|
-
INSTANCE "Cover" (1:1564) sym=1:1322 overrides=3
|
|
76
|
-
SLIDE "2" (1:1570)
|
|
77
|
-
INSTANCE "Content" (1:1572) sym=1:1129 overrides=7
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Filter by node type (`--type SLIDE`, `--type INSTANCE`, `--type SYMBOL`) or limit depth. Use `--json` for machine-readable output.
|
|
81
|
-
|
|
82
|
-
### `list-text` — All content
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
figmatk list-text file.deck
|
|
44
|
+
figmatk inspect my-presentation.deck # node hierarchy
|
|
45
|
+
figmatk list-text my-presentation.deck # all text + images per slide
|
|
46
|
+
figmatk list-overrides my-presentation.deck # editable fields per symbol
|
|
86
47
|
```
|
|
87
48
|
|
|
88
|
-
|
|
49
|
+
→ Full CLI reference: [docs/cli.md](docs/cli.md)
|
|
89
50
|
|
|
90
|
-
|
|
91
|
-
SLIDE "1" → INSTANCE (1:2001) sym=1:1322
|
|
92
|
-
57:48 TEXT: "My Presentation Title"
|
|
93
|
-
57:49 TEXT: "Subtitle Goes Here"
|
|
94
|
-
75:126 IMAGE: 780960f6236bd1305ceeb2590ca395e36e705816 (1011x621)
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### `list-overrides` — Editable fields
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
figmatk list-overrides file.deck [--symbol "Symbol Name"]
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
For every symbol (component) in the file, lists each node that has an `overrideKey` — these are the fields you can modify via `symbolOverrides`. Shows the key ID, node type, name, and current default value.
|
|
104
|
-
|
|
105
|
-
```
|
|
106
|
-
SYMBOL "Image+Text" (1:1205)
|
|
107
|
-
75:126 ROUNDED_RECTANGLE "Photo location" [IMAGE PLACEHOLDER]
|
|
108
|
-
75:127 TEXT "Header" → "Header"
|
|
109
|
-
75:131 TEXT "Subtitle" → "SUBTITLE 2"
|
|
110
|
-
75:132 TEXT "Body" → "Body small lorem ipsum..."
|
|
111
|
-
```
|
|
51
|
+
## Claude Code / MCP Integration
|
|
112
52
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
```bash
|
|
116
|
-
figmatk update-text input.deck -o output.deck \
|
|
117
|
-
--slide 1:2000 \
|
|
118
|
-
--set "57:48=New Title" \
|
|
119
|
-
--set "57:49=New Subtitle"
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Finds the slide (by node ID or name), locates its instance, and adds or updates text overrides. Repeat `--set` for multiple fields. Empty strings are auto-replaced with a space (empty string crashes Figma).
|
|
123
|
-
|
|
124
|
-
### `insert-image` — Place images
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
figmatk insert-image input.deck -o output.deck \
|
|
128
|
-
--slide 1:2006 \
|
|
129
|
-
--key 75:126 \
|
|
130
|
-
--image screenshot.png \
|
|
131
|
-
[--thumb thumbnail.png]
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Overrides an image placeholder on a slide instance. Automatically:
|
|
135
|
-
- SHA-1 hashes the image and copies it to the `images/` directory
|
|
136
|
-
- Generates a ~320px thumbnail (or uses `--thumb` if provided)
|
|
137
|
-
- Sets the required `styleIdForFill` sentinel GUID
|
|
138
|
-
- Sets `imageThumbnail` with the thumbnail hash
|
|
139
|
-
|
|
140
|
-
### `clone-slide` — Duplicate with content
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
figmatk clone-slide input.deck -o output.deck \
|
|
144
|
-
--template 1:1559 \
|
|
145
|
-
--name "New Slide" \
|
|
146
|
-
--set "57:48=Title" \
|
|
147
|
-
--set "57:49=Subtitle" \
|
|
148
|
-
--set-image "75:126=photo.png"
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Deep-clones a slide + instance pair from a template, assigns fresh GUIDs, applies text and image overrides, and appends to the deck. Uses `Uint8Array`-safe cloning (not `JSON.parse/stringify`).
|
|
152
|
-
|
|
153
|
-
### `remove-slide` — Delete slides
|
|
154
|
-
|
|
155
|
-
```bash
|
|
156
|
-
figmatk remove-slide input.deck -o output.deck \
|
|
157
|
-
--slide 1:1769 \
|
|
158
|
-
--slide 1:1732
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
Marks slides and their child instances as `REMOVED`. Repeat `--slide` for multiple. Nodes are never deleted from the array — Figma requires them to remain with `phase: 'REMOVED'`.
|
|
162
|
-
|
|
163
|
-
### `roundtrip` — Validate the pipeline
|
|
164
|
-
|
|
165
|
-
```bash
|
|
166
|
-
figmatk roundtrip input.deck -o output.deck
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
Decodes and re-encodes with zero changes. If Figma opens the output, your pipeline is sound. Prints node/slide/blob counts.
|
|
170
|
-
|
|
171
|
-
## Claude Cowork / Claude Code Integration
|
|
172
|
-
|
|
173
|
-
FigmaTK ships as a **Cowork plugin** with an MCP server. This lets Claude manipulate `.deck` files directly as tool calls.
|
|
174
|
-
|
|
175
|
-
### Install as plugin
|
|
53
|
+
FigmaTK ships as a **Cowork plugin** with an MCP server — Claude can manipulate `.deck` files directly as tool calls.
|
|
176
54
|
|
|
177
55
|
```bash
|
|
178
56
|
claude plugin marketplace add rcoenen/figmatk
|
|
179
57
|
claude plugin install figmatk
|
|
180
58
|
```
|
|
181
59
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
In Claude Desktop → Settings → Developer → Edit Config:
|
|
60
|
+
Or add manually in Claude Desktop → Settings → Developer → Edit Config:
|
|
185
61
|
|
|
186
62
|
```json
|
|
187
63
|
{
|
|
188
64
|
"mcpServers": {
|
|
189
|
-
"figmatk": {
|
|
190
|
-
"command": "figmatk-mcp"
|
|
191
|
-
}
|
|
65
|
+
"figmatk": { "command": "figmatk-mcp" }
|
|
192
66
|
}
|
|
193
67
|
}
|
|
194
68
|
```
|
|
195
69
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
| Tool | Description |
|
|
199
|
-
|------|-------------|
|
|
200
|
-
| `figmatk_inspect` | Show node hierarchy tree |
|
|
201
|
-
| `figmatk_list_text` | List all text and image content per slide |
|
|
202
|
-
| `figmatk_list_overrides` | List editable override keys per symbol |
|
|
203
|
-
| `figmatk_update_text` | Apply text overrides to a slide instance |
|
|
204
|
-
| `figmatk_insert_image` | Apply image fill override |
|
|
205
|
-
| `figmatk_clone_slide` | Duplicate a slide |
|
|
206
|
-
| `figmatk_remove_slide` | Mark a slide as REMOVED |
|
|
207
|
-
| `figmatk_roundtrip` | Decode and re-encode for validation |
|
|
70
|
+
Available MCP tools: `figmatk_inspect`, `figmatk_list_text`, `figmatk_list_overrides`, `figmatk_update_text`, `figmatk_insert_image`, `figmatk_clone_slide`, `figmatk_remove_slide`, `figmatk_roundtrip`.
|
|
208
71
|
|
|
209
|
-
##
|
|
72
|
+
## Programmatic API
|
|
210
73
|
|
|
211
74
|
```javascript
|
|
212
|
-
import {
|
|
213
|
-
import { ov, nestedOv, removeNode } from 'figmatk/node-helpers';
|
|
214
|
-
import { imageOv } from 'figmatk/image-helpers';
|
|
215
|
-
import { deepClone } from 'figmatk/deep-clone';
|
|
216
|
-
|
|
217
|
-
// Load
|
|
218
|
-
const deck = await FigDeck.fromDeckFile('template.deck');
|
|
219
|
-
|
|
220
|
-
// Explore
|
|
221
|
-
console.log(deck.getActiveSlides().length, 'slides');
|
|
222
|
-
console.log(deck.getSymbols().map(s => s.name));
|
|
223
|
-
|
|
224
|
-
// Walk the tree
|
|
225
|
-
deck.walkTree('0:0', (node, depth) => {
|
|
226
|
-
console.log(' '.repeat(depth) + node.type + ' ' + (node.name || ''));
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// Find a slide's instance and read its overrides
|
|
230
|
-
const slide = deck.getActiveSlides()[0];
|
|
231
|
-
const inst = deck.getSlideInstance('1:2000');
|
|
232
|
-
console.log(inst.symbolData.symbolOverrides);
|
|
233
|
-
|
|
234
|
-
// Save
|
|
235
|
-
await deck.saveDeck('output.deck');
|
|
236
|
-
```
|
|
75
|
+
import { Deck } from 'figmatk';
|
|
237
76
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
| `lib/fig-deck.mjs` | `FigDeck` | Core class — parse, query, encode, save |
|
|
243
|
-
| `lib/node-helpers.mjs` | `nid(node)` | Format node ID as `"sessionID:localID"` |
|
|
244
|
-
| | `parseId(str)` | Parse `"57:48"` to `{ sessionID, localID }` |
|
|
245
|
-
| | `ov(key, text)` | Build a text override for `symbolOverrides` |
|
|
246
|
-
| | `nestedOv(instKey, textKey, text)` | Text override for nested instances |
|
|
247
|
-
| | `removeNode(node)` | Mark node as REMOVED |
|
|
248
|
-
| `lib/image-helpers.mjs` | `imageOv(key, hash, thumbHash, w, h)` | Build a complete image fill override |
|
|
249
|
-
| | `hexToHash(hex)` / `hashToHex(arr)` | Convert between hex strings and `Uint8Array(20)` |
|
|
250
|
-
| `lib/deep-clone.mjs` | `deepClone(obj)` | `Uint8Array`-safe deep clone |
|
|
251
|
-
|
|
252
|
-
### FigDeck API
|
|
253
|
-
|
|
254
|
-
| Method | Returns | Description |
|
|
255
|
-
|--------|---------|-------------|
|
|
256
|
-
| `FigDeck.fromDeckFile(path)` | `Promise<FigDeck>` | Load from `.deck` ZIP |
|
|
257
|
-
| `FigDeck.fromFigFile(path)` | `FigDeck` | Load from raw `.fig` |
|
|
258
|
-
| `deck.getSlides()` | `node[]` | All SLIDE nodes |
|
|
259
|
-
| `deck.getActiveSlides()` | `node[]` | Non-REMOVED slides |
|
|
260
|
-
| `deck.getInstances()` | `node[]` | All INSTANCE nodes |
|
|
261
|
-
| `deck.getSymbols()` | `node[]` | All SYMBOL nodes |
|
|
262
|
-
| `deck.getNode(id)` | `node` | Lookup by `"s:l"` string |
|
|
263
|
-
| `deck.getChildren(id)` | `node[]` | Child nodes |
|
|
264
|
-
| `deck.getSlideInstance(slideId)` | `node` | INSTANCE child of a SLIDE |
|
|
265
|
-
| `deck.walkTree(rootId, fn)` | void | DFS traversal |
|
|
266
|
-
| `deck.maxLocalID()` | `number` | Highest ID in use |
|
|
267
|
-
| `deck.rebuildMaps()` | void | Re-index after mutations |
|
|
268
|
-
| `deck.encodeFig()` | `Promise<Uint8Array>` | Encode to `canvas.fig` binary |
|
|
269
|
-
| `deck.saveDeck(path, opts?)` | `Promise<number>` | Write complete `.deck` ZIP |
|
|
270
|
-
| `deck.saveFig(path)` | `Promise<void>` | Write raw `.fig` binary |
|
|
271
|
-
|
|
272
|
-
## `.deck` File Format
|
|
273
|
-
|
|
274
|
-
See **[docs/deck-format.md](docs/deck-format.md)** for the full binary format specification — archive structure, chunk layout, node types, symbol overrides, image override requirements, cloning rules, and all known format constraints.
|
|
275
|
-
|
|
276
|
-
## Architecture
|
|
277
|
-
|
|
278
|
-
```
|
|
279
|
-
figmatk/
|
|
280
|
-
cli.mjs # CLI entry point — arg parsing + subcommand dispatch
|
|
281
|
-
mcp-server.mjs # MCP server for Claude Cowork / Claude Code
|
|
282
|
-
lib/
|
|
283
|
-
fig-deck.mjs # FigDeck class — own binary parser, no third-party deps
|
|
284
|
-
deep-clone.mjs # Uint8Array-safe recursive deep clone
|
|
285
|
-
node-helpers.mjs # Node ID utils, override builders, removal helper
|
|
286
|
-
image-helpers.mjs # SHA-1 hash conversion, image override builder
|
|
287
|
-
commands/
|
|
288
|
-
inspect.mjs # Tree view of document structure
|
|
289
|
-
list-text.mjs # All text + image content per slide
|
|
290
|
-
list-overrides.mjs # Editable override keys per symbol
|
|
291
|
-
update-text.mjs # Set text overrides on a slide
|
|
292
|
-
insert-image.mjs # Image fill override with auto-thumbnail
|
|
293
|
-
clone-slide.mjs # Duplicate a slide with content
|
|
294
|
-
remove-slide.mjs # Mark slides REMOVED
|
|
295
|
-
roundtrip.mjs # Decode/re-encode validation
|
|
296
|
-
skills/
|
|
297
|
-
figmatk/SKILL.md # Cowork skill definition
|
|
298
|
-
.claude-plugin/
|
|
299
|
-
plugin.json # Cowork plugin manifest
|
|
300
|
-
marketplace.json # Plugin marketplace listing
|
|
301
|
-
.mcp.json # MCP server config
|
|
77
|
+
const deck = await Deck.open('template.deck');
|
|
78
|
+
const slide = deck.slides[0];
|
|
79
|
+
slide.addText('Hello world', { style: 'Title' });
|
|
80
|
+
await deck.save('output.deck');
|
|
302
81
|
```
|
|
303
82
|
|
|
304
|
-
|
|
83
|
+
| Docs | |
|
|
84
|
+
|------|---|
|
|
85
|
+
| High-level API | [docs/figmatk-api-spec.md](docs/figmatk-api-spec.md) |
|
|
86
|
+
| Low-level FigDeck API | [docs/library.md](docs/library.md) |
|
|
87
|
+
| File format internals | [docs/format/](docs/format/) |
|
|
305
88
|
|
|
306
89
|
## License
|
|
307
90
|
|
package/lib/api.mjs
CHANGED
|
@@ -56,7 +56,10 @@ export class Deck {
|
|
|
56
56
|
const templatePath = join(__dirname, 'blank-template.deck');
|
|
57
57
|
const fd = await FigDeck.fromDeckFile(templatePath);
|
|
58
58
|
fd.deckMeta = { file_name: opts.name ?? 'Untitled', version: '1' };
|
|
59
|
-
|
|
59
|
+
const deck = new Deck(fd, null);
|
|
60
|
+
// Remember the template's blank slide so addBlankSlide() can auto-remove it
|
|
61
|
+
deck._templateSlide = fd.getActiveSlides()[0] ?? null;
|
|
62
|
+
return deck;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
/** Presentation metadata from meta.json */
|
|
@@ -154,9 +157,15 @@ export class Deck {
|
|
|
154
157
|
fd.message.nodeChanges.push(newSlide);
|
|
155
158
|
fd.rebuildMaps();
|
|
156
159
|
|
|
160
|
+
// Auto-remove the original template blank slide on first addBlankSlide() call
|
|
161
|
+
if (this._templateSlide) {
|
|
162
|
+
this._templateSlide.phase = 'REMOVED';
|
|
163
|
+
this._templateSlide = null;
|
|
164
|
+
fd.rebuildMaps();
|
|
165
|
+
}
|
|
166
|
+
|
|
157
167
|
const slide = new Slide(fd, newSlide);
|
|
158
168
|
|
|
159
|
-
// Apply background if specified
|
|
160
169
|
if (opts.background) {
|
|
161
170
|
slide.setBackground(opts.background);
|
|
162
171
|
}
|
|
@@ -328,18 +337,13 @@ export class Slide {
|
|
|
328
337
|
const opacity = opts.opacity ?? 1;
|
|
329
338
|
let rgb, colorVar;
|
|
330
339
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
};
|
|
339
|
-
} else {
|
|
340
|
-
rgb = { r: color.r, g: color.g, b: color.b, a: color.a ?? 1 };
|
|
341
|
-
colorVar = undefined;
|
|
342
|
-
}
|
|
340
|
+
const parsed = parseColor(this._fd, color);
|
|
341
|
+
rgb = { r: parsed.r, g: parsed.g, b: parsed.b, a: 1 };
|
|
342
|
+
colorVar = parsed._guid ? {
|
|
343
|
+
value: { alias: { guid: deepClone(parsed._guid) } },
|
|
344
|
+
dataType: 'ALIAS',
|
|
345
|
+
resolvedDataType: 'COLOR',
|
|
346
|
+
} : undefined;
|
|
343
347
|
|
|
344
348
|
const fill = {
|
|
345
349
|
type: 'SOLID',
|
|
@@ -799,7 +803,7 @@ export class Slide {
|
|
|
799
803
|
textData.lines = buildLines(chars, isRuns ? textOrRuns : null, opts.list);
|
|
800
804
|
}
|
|
801
805
|
|
|
802
|
-
const fillColor = opts.color ??
|
|
806
|
+
const fillColor = parseColor(fd, opts.color ?? 'black');
|
|
803
807
|
|
|
804
808
|
const node = {
|
|
805
809
|
guid: { sessionID: 1, localID },
|
|
@@ -949,7 +953,7 @@ export class Slide {
|
|
|
949
953
|
addLine(x1, y1, x2, y2, opts = {}) {
|
|
950
954
|
const fd = this._fd;
|
|
951
955
|
const localID = fd.maxLocalID() + 1;
|
|
952
|
-
const color = opts.color ??
|
|
956
|
+
const color = parseColor(fd, opts.color ?? 'black');
|
|
953
957
|
|
|
954
958
|
const dx = x2 - x1;
|
|
955
959
|
const dy = y2 - y1;
|
|
@@ -1441,9 +1445,10 @@ export class Shape {
|
|
|
1441
1445
|
* @param {number} [opts.opacity] - Fill opacity 0-1 (default: 1)
|
|
1442
1446
|
*/
|
|
1443
1447
|
setFill(color, opts = {}) {
|
|
1448
|
+
const c = parseColor(this._fd, color);
|
|
1444
1449
|
const paint = [{
|
|
1445
1450
|
type: 'SOLID',
|
|
1446
|
-
color: { r:
|
|
1451
|
+
color: { r: c.r, g: c.g, b: c.b, a: 1 },
|
|
1447
1452
|
opacity: opts.opacity ?? 1,
|
|
1448
1453
|
visible: true,
|
|
1449
1454
|
blendMode: 'NORMAL',
|
|
@@ -1482,9 +1487,10 @@ export class Shape {
|
|
|
1482
1487
|
* @param {string} [opts.align] - 'INSIDE' | 'OUTSIDE' | 'CENTER' (default: 'INSIDE')
|
|
1483
1488
|
*/
|
|
1484
1489
|
setStroke(color, opts = {}) {
|
|
1490
|
+
const c = parseColor(this._fd, color);
|
|
1485
1491
|
this._node.strokePaints = [{
|
|
1486
1492
|
type: 'SOLID',
|
|
1487
|
-
color: { r:
|
|
1493
|
+
color: { r: c.r, g: c.g, b: c.b, a: 1 },
|
|
1488
1494
|
opacity: 1,
|
|
1489
1495
|
visible: true,
|
|
1490
1496
|
blendMode: 'NORMAL',
|
|
@@ -1621,10 +1627,59 @@ function resolveTextStyle(fd, styleName) {
|
|
|
1621
1627
|
throw new Error(`Unknown text style: "${styleName}". Available: Title, Header 1, Header 2, Header 3, Body 1, Body 2, Body 3, Note`);
|
|
1622
1628
|
}
|
|
1623
1629
|
|
|
1630
|
+
// Designer-friendly color aliases → hex
|
|
1631
|
+
const DESIGNER_COLORS = {
|
|
1632
|
+
// Neutrals
|
|
1633
|
+
white: '#FFFFFF', black: '#000000', cream: '#F5F0E8', ivory: '#FFFFF0',
|
|
1634
|
+
charcoal: '#36454F', smoke: '#F5F5F5', silver: '#C0C0C0', ash: '#B2BEB5',
|
|
1635
|
+
// Blues
|
|
1636
|
+
navy: '#1E2761', midnight: '#0D1B2A', cobalt: '#0047AB', sky: '#87CEEB',
|
|
1637
|
+
teal: '#008080', cyan: '#00BCD4', steel: '#4682B4', denim: '#1560BD',
|
|
1638
|
+
// Greens
|
|
1639
|
+
forest: '#2C5F2D', sage: '#A7BEAE', mint: '#98FF98', olive: '#808000',
|
|
1640
|
+
emerald: '#50C878', moss: '#8A9A5B', lime: '#32CD32',
|
|
1641
|
+
// Reds / warm
|
|
1642
|
+
coral: '#F96167', crimson: '#DC143C', rose: '#FF007F', blush: '#FFB6C1',
|
|
1643
|
+
burgundy: '#800020', brick: '#CB4154', salmon: '#FA8072',
|
|
1644
|
+
terracotta: '#B85042', rust: '#B7410E', sand: '#E7E8D1',
|
|
1645
|
+
amber: '#FFBF00', gold: '#FFD700', saffron: '#F4C430', peach: '#FFCBA4',
|
|
1646
|
+
// Purples
|
|
1647
|
+
lavender: '#E6E6FA', violet: '#8B00FF', plum: '#DDA0DD',
|
|
1648
|
+
mauve: '#E0B0FF', indigo: '#4B0082', grape: '#6F2DA8', purple: '#800080',
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
function _hexToRgb(hex) {
|
|
1652
|
+
const h = hex.replace('#', '');
|
|
1653
|
+
return { r: parseInt(h.slice(0,2),16)/255, g: parseInt(h.slice(2,4),16)/255, b: parseInt(h.slice(4,6),16)/255 };
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1624
1656
|
/**
|
|
1625
|
-
* Resolve
|
|
1626
|
-
*
|
|
1657
|
+
* Resolve any color value to { r, g, b } (0-1) plus optional colorVar for Figma variables.
|
|
1658
|
+
* Accepts:
|
|
1659
|
+
* - Designer alias: 'teal', 'coral', 'navy', 'midnight', ...
|
|
1660
|
+
* - Hex string: '#E63946' or 'E63946'
|
|
1661
|
+
* - Figma theme: 'Blue', 'Red', 'Slate', ... (from Light Slides variables)
|
|
1662
|
+
* - Raw object: { r, g, b } normalized 0-1
|
|
1627
1663
|
*/
|
|
1664
|
+
function parseColor(fd, color) {
|
|
1665
|
+
if (!color && color !== 0) return { r: 0, g: 0, b: 0 };
|
|
1666
|
+
if (typeof color === 'object') return { r: color.r, g: color.g, b: color.b };
|
|
1667
|
+
if (typeof color === 'string') {
|
|
1668
|
+
// Hex string
|
|
1669
|
+
if (/^#?[0-9a-fA-F]{6}$/.test(color)) return _hexToRgb(color);
|
|
1670
|
+
// Figma theme variable first (exact match, preserves colorVar binding for slides)
|
|
1671
|
+
try {
|
|
1672
|
+
const variable = resolveColorVariable(fd, color);
|
|
1673
|
+
return { r: variable.r, g: variable.g, b: variable.b, _guid: variable.guid };
|
|
1674
|
+
} catch (_) {}
|
|
1675
|
+
// Designer alias fallback (case-insensitive)
|
|
1676
|
+
const alias = DESIGNER_COLORS[color.toLowerCase()];
|
|
1677
|
+
if (alias) return _hexToRgb(alias);
|
|
1678
|
+
throw new Error(`Unknown color: "${color}". Use a Light Slides name ('Black', 'Teal'), a designer alias ('navy', 'coral'), or a hex string ('#E63946').`);
|
|
1679
|
+
}
|
|
1680
|
+
throw new Error(`Invalid color: ${JSON.stringify(color)}`);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1628
1683
|
/**
|
|
1629
1684
|
* Resolve a named color (e.g. 'Blue', 'Red') to its VARIABLE node.
|
|
1630
1685
|
* Returns { guid, r, g, b } from the Light Slides color variable set.
|
package/package.json
CHANGED
package/skills/figmatk/SKILL.md
CHANGED
|
@@ -1,39 +1,204 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: figmatk
|
|
3
3
|
description: >
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"list slide content", or works with .deck or .fig files.
|
|
4
|
+
Create, edit, and inspect Figma Slides .deck files. Use when the user asks to
|
|
5
|
+
create a presentation, build a slide deck, edit slides, update text or images,
|
|
6
|
+
clone or remove slides, or produce a .deck file for Figma Slides.
|
|
8
7
|
metadata:
|
|
9
|
-
version: "0.0.
|
|
8
|
+
version: "0.0.7"
|
|
10
9
|
---
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
# FigmaTK Skill
|
|
13
12
|
|
|
14
|
-
##
|
|
13
|
+
## Quick Reference
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- `figmatk_remove_slide` — Mark a slide as REMOVED
|
|
23
|
-
- `figmatk_roundtrip` — Decode and re-encode for validation
|
|
15
|
+
| Task | Approach |
|
|
16
|
+
|------|----------|
|
|
17
|
+
| Create a new deck from scratch | Use the high-level JS API (`lib/api.mjs`) |
|
|
18
|
+
| Edit text or images in an existing deck | Use MCP tools (`figmatk_update_text`, `figmatk_insert_image`) |
|
|
19
|
+
| Clone, remove, or restructure slides | Use MCP tools (`figmatk_clone_slide`, `figmatk_remove_slide`) |
|
|
20
|
+
| Inspect structure or read content | Use MCP tools (`figmatk_inspect`, `figmatk_list_text`) |
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Path A — Create from Scratch (High-Level API)
|
|
25
|
+
|
|
26
|
+
Use this when the user wants a new presentation. Write a Node.js script and execute it.
|
|
27
|
+
|
|
28
|
+
> **Import path:** `figmatk` is an npm package. Import from the installed package:
|
|
29
|
+
> ```javascript
|
|
30
|
+
> import { Deck } from 'figmatk';
|
|
31
|
+
> ```
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
import { Deck } from 'figmatk';
|
|
35
|
+
|
|
36
|
+
const deck = await Deck.create('My Presentation');
|
|
37
|
+
|
|
38
|
+
const slide = deck.addBlankSlide(); // template blank slide auto-removed
|
|
39
|
+
slide.setBackground('Black'); // named color — see list below
|
|
40
|
+
slide.addText('Slide Title', {
|
|
41
|
+
style: 'Title', color: 'White',
|
|
42
|
+
x: 64, y: 80, width: 1792, align: 'LEFT'
|
|
43
|
+
});
|
|
44
|
+
slide.addText('Subtitle', {
|
|
45
|
+
style: 'Body 1', color: 'Grey',
|
|
46
|
+
x: 64, y: 240, width: 1200, align: 'LEFT'
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await deck.save('/path/to/output.deck');
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### ⚠️ Critical gotchas
|
|
53
|
+
|
|
54
|
+
| Issue | Wrong | Right |
|
|
55
|
+
|-------|-------|-------|
|
|
56
|
+
| `setBackground` with hex | `s.setBackground('#1A1A1A')` | `s.setBackground('Black')` |
|
|
57
|
+
| `setBackground` with raw RGB | `s.setBackground({ r:0.1, g:0.1, b:0.1 })` | `s.setBackground('Black')` — raw RGB silently renders white |
|
|
58
|
+
| Shape method signature | `s.addRectangle({ x:0, y:0, width:100 })` | `s.addRectangle(0, 0, 100, 100, opts)` |
|
|
59
|
+
| Shape fill color | `{ fill: '#F4900C' }` | `{ fill: hex('#F4900C') }` — use the hex() helper |
|
|
60
|
+
| `addLine` options | `{ strokeColor: ..., strokeWeight: 2 }` | `{ color: 'Black', weight: 2 }` |
|
|
61
|
+
| `align` value | `align: 'left'` | `align: 'LEFT'` (uppercase) |
|
|
62
|
+
|
|
63
|
+
### Hex color helper (for shape fills)
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
function hex(h) {
|
|
67
|
+
return { r: parseInt(h.slice(1,3),16)/255, g: parseInt(h.slice(3,5),16)/255, b: parseInt(h.slice(5,7),16)/255 };
|
|
68
|
+
}
|
|
69
|
+
// Usage: s.addRectangle(0, 0, 200, 50, { fill: hex('#F4900C') })
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Text styles
|
|
73
|
+
|
|
74
|
+
| Style | Size | Weight | Use for |
|
|
75
|
+
|-------|------|--------|---------|
|
|
76
|
+
| `Title` | 96pt | Bold | Slide title |
|
|
77
|
+
| `Header 1` | 60pt | Bold | Section headers |
|
|
78
|
+
| `Header 2` | 48pt | Bold | Sub-headers |
|
|
79
|
+
| `Header 3` | 36pt | Bold | In-slide headings |
|
|
80
|
+
| `Body 1` | 36pt | Regular | Primary body text |
|
|
81
|
+
| `Body 2` | 30pt | Regular | Secondary body text |
|
|
82
|
+
| `Body 3` | 24pt | Regular | Captions, labels |
|
|
83
|
+
| `Note` | 20pt | Regular | Footnotes, sources |
|
|
84
|
+
|
|
85
|
+
### Named colors for `setBackground()`
|
|
86
|
+
|
|
87
|
+
> **Case-sensitive.** `'Black'` works, `'black'` does not.
|
|
88
|
+
|
|
89
|
+
`'Black'`, `'White'`, `'Grey'`, `'Blue'`, `'Red'`, `'Yellow'`, `'Green'`, `'Orange'`, `'Pink'`, `'Purple'`, `'Teal'`, `'Violet'`, `'Persimmon'`, `'Pale Pink'`, `'Pale Blue'`, `'Pale Green'`, `'Pale Teal'`, `'Pale Purple'`, `'Pale Persimmon'`, `'Pale Violet'`, `'Pale Red'`, `'Pale Yellow'`
|
|
90
|
+
|
|
91
|
+
Use `'Black'` for dark backgrounds, `'White'` for light. For custom slide backgrounds, use the closest named color — **not hex**.
|
|
92
|
+
|
|
93
|
+
### Slide dimensions
|
|
94
|
+
|
|
95
|
+
1920 × 1080px. All positions and sizes in pixels.
|
|
96
|
+
|
|
97
|
+
### Slide methods (correct signatures)
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
slide.setBackground(namedColor) // named color only — hex/raw RGB render white
|
|
101
|
+
slide.addText(text, opts) // opts: style, color (named or hex('#...')), x, y, width, align, bold, italic, fontSize
|
|
102
|
+
slide.addFrame(opts) // auto-layout: stackMode, spacing, x, y, width, height
|
|
103
|
+
slide.addRectangle(x, y, width, height, opts) // opts: fill (named or {r,g,b}), opacity, cornerRadius
|
|
104
|
+
slide.addEllipse(x, y, width, height, opts) // opts: fill, opacity
|
|
105
|
+
slide.addDiamond(x, y, width, height, opts)
|
|
106
|
+
slide.addTriangle(x, y, width, height, opts)
|
|
107
|
+
slide.addStar(x, y, width, height, opts)
|
|
108
|
+
slide.addLine(x1, y1, x2, y2, opts) // opts: color, weight
|
|
109
|
+
slide.addImage(path, opts) // opts: x, y, width, height
|
|
110
|
+
slide.addTable(data, opts) // 2D string array; opts: x, y, width, colWidths, rowHeight
|
|
111
|
+
slide.addSVG(x, y, width, svgPathOrBuf, opts)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Path B — Edit an Existing Deck (MCP Tools)
|
|
26
117
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
118
|
+
Use this when the user provides a `.deck` file to modify.
|
|
119
|
+
|
|
120
|
+
### Workflow
|
|
121
|
+
|
|
122
|
+
1. `figmatk_inspect` — understand the deck structure (node IDs, slide count, symbols)
|
|
123
|
+
2. `figmatk_list_text` — read current text and images per slide
|
|
124
|
+
3. `figmatk_list_overrides` — find the override keys for each symbol (what's editable)
|
|
125
|
+
4. `figmatk_update_text` — apply text changes
|
|
126
|
+
5. `figmatk_insert_image` — apply image changes
|
|
127
|
+
6. `figmatk_clone_slide` — duplicate a slide and populate it
|
|
128
|
+
7. `figmatk_remove_slide` — mark unwanted slides as REMOVED
|
|
129
|
+
8. Always write to a **new output path** — never overwrite the source
|
|
130
|
+
|
|
131
|
+
### MCP tool reference
|
|
132
|
+
|
|
133
|
+
| Tool | Purpose |
|
|
134
|
+
|------|---------|
|
|
135
|
+
| `figmatk_inspect` | Node hierarchy tree — structure, node IDs, slide count |
|
|
136
|
+
| `figmatk_list_text` | All text strings and image hashes per slide |
|
|
137
|
+
| `figmatk_list_overrides` | Editable override keys per symbol (component) |
|
|
138
|
+
| `figmatk_update_text` | Set text overrides on a slide instance |
|
|
139
|
+
| `figmatk_insert_image` | Set image fill override (handles SHA-1 hashing + thumbnail) |
|
|
140
|
+
| `figmatk_clone_slide` | Deep-clone a slide with new text and images |
|
|
141
|
+
| `figmatk_remove_slide` | Mark slides as REMOVED (never deleted) |
|
|
142
|
+
| `figmatk_roundtrip` | Decode + re-encode for pipeline validation |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Design Philosophy
|
|
147
|
+
|
|
148
|
+
Every deck must look **intentionally designed**, not AI-generated.
|
|
149
|
+
|
|
150
|
+
### Colour
|
|
151
|
+
|
|
152
|
+
- Pick a bold palette for the **specific topic** — not a generic one.
|
|
153
|
+
- One dominant colour (60–70%) + 1–2 supporting tones + one sharp accent.
|
|
154
|
+
- Dark backgrounds on title/conclusion slides, light on content ("sandwich") — or fully dark for premium feel.
|
|
155
|
+
|
|
156
|
+
**Starter palettes** (use nearest named color for `setBackground`, hex helper for shapes):
|
|
157
|
+
|
|
158
|
+
| Theme | Background | Shape accent | Text |
|
|
159
|
+
|-------|-----------|-------------|------|
|
|
160
|
+
| Midnight | `'Black'` | `hex('#CADCFC')` | `'White'` |
|
|
161
|
+
| Forest | `'Green'` | `hex('#97BC62')` | `'White'` |
|
|
162
|
+
| Coral | `'Persimmon'` | `hex('#2F3C7E')` | `'White'` |
|
|
163
|
+
| Terracotta | `'Persimmon'` | `hex('#E7E8D1')` | `'White'` |
|
|
164
|
+
| Ocean | `'Blue'` | `hex('#21295C')` | `'White'` |
|
|
165
|
+
| Minimal | `'White'` | `hex('#36454F')` | `'Black'` |
|
|
166
|
+
|
|
167
|
+
### Layout
|
|
168
|
+
|
|
169
|
+
- Every slide needs at least **one visual element** — shape, image, SVG, or table.
|
|
170
|
+
- **Vary layouts** — never repeat the same structure slide after slide.
|
|
171
|
+
- Carry one visual motif through every slide (coloured accent bar, icon circles, etc.).
|
|
172
|
+
|
|
173
|
+
**Layout options:** two-column, icon+text rows, 2×2/2×3 grid, large stat callout, half-background image, timeline/steps.
|
|
174
|
+
|
|
175
|
+
### Typography
|
|
176
|
+
|
|
177
|
+
- Left-align body text. Centre only titles.
|
|
178
|
+
- Minimum 64px margin from slide edges. 24–48px between content blocks.
|
|
179
|
+
|
|
180
|
+
### Never do
|
|
181
|
+
|
|
182
|
+
- Repeat the same layout slide after slide
|
|
183
|
+
- Centre body text
|
|
184
|
+
- Use accent lines under slide titles (hallmark of AI-generated slides)
|
|
185
|
+
- Text-only slides
|
|
186
|
+
- Low-contrast text against background
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## QA
|
|
191
|
+
|
|
192
|
+
1. Self-check: no placeholder text (`lorem ipsum`, `[title here]`) remains
|
|
193
|
+
2. Tell the user to open the `.deck` in Figma Desktop to catch rendering issues
|
|
194
|
+
3. Offer to fix anything they report
|
|
195
|
+
|
|
196
|
+
---
|
|
32
197
|
|
|
33
|
-
## Critical
|
|
198
|
+
## Critical Format Rules
|
|
34
199
|
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
200
|
+
- Blank text must be `" "` (space), never `""` — empty string crashes Figma
|
|
201
|
+
- Image overrides need both a full-image hash and thumbnail hash (40-char hex SHA-1)
|
|
202
|
+
- Removed nodes: set `phase: 'REMOVED'`, never delete from `nodeChanges`
|
|
203
|
+
- Chunk 1 of `canvas.fig` must be zstd-compressed
|
|
204
|
+
- `thumbHash` must be `new Uint8Array(0)`, never `{}`
|