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.
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "name": "figmatk",
15
15
  "description": "Inspect and modify Figma Slides .deck files natively",
16
- "version": "0.0.5",
16
+ "version": "0.0.6",
17
17
  "author": {
18
18
  "name": "FigmaTK Contributors"
19
19
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "figmatk",
3
- "version": "0.0.3",
4
- "description": "Inspect and modify Figma Slides .deck files natively — no lossy .pptx conversion",
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` and `.fig` files. Parse, inspect, modify, and rebuild Figma Slides decks programmatically — no Figma API required.
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 Design | `.fig` |
12
- | Figma Slides | `.deck` |
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 and re-upload them. This is the **native** round-trip format. The alternative, exporting to `.pptx`, is lossy: vectors get rasterized to bitmaps, fonts fall back to system defaults, and precise layout breaks. By staying in `.deck`, you preserve everything — fonts, vector shapes, component overrides, styles — exactly as Figma renders them.
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
- **FigmaTK** makes this round-trip programmable. Download a `.deck`, modify it with these utilities, re-upload to Figma. Everything stays native.
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), [Codex](https://openai.com/index/openai-codex/), 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.
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 read slide content, rewrite copy, insert images, and produce a ready-to-upload `.deck` without ever touching the Figma UI
31
- - **Batch-produce branded decks** — start from a company template, feed in data per client/project, get pixel-perfect slides out
32
- - **Inspect and audit** — understand the internal structure of any `.deck` or `.fig` file
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 (`.mjs`). Node 18+.
39
+ Node 18+. No build step. Pure ESM.
43
40
 
44
41
  ## Quick Start
45
42
 
46
43
  ```bash
47
- # See what's inside a deck
48
- figmatk inspect my-presentation.deck
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
- Shows every text string and image hash in the deck — both direct node text and symbol override text. Useful for auditing content or extracting copy.
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
- ### `update-text`Change text
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
- ### Or add as MCP server manually
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
- ### Available MCP tools
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
- ## Using as a Library
72
+ ## Programmatic API
210
73
 
211
74
  ```javascript
212
- import { FigDeck } from 'figmatk/deck';
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
- ### Key classes and functions
239
-
240
- | Module | Export | Description |
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
- Six npm packages: `kiwi-schema`, `fzstd`, `zstd-codec`, `pako`, `archiver`, `@modelcontextprotocol/sdk`.
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
- return new Deck(fd, null);
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
- if (typeof color === 'string') {
332
- const variable = resolveColorVariable(this._fd, color);
333
- rgb = { r: variable.r, g: variable.g, b: variable.b, a: 1 };
334
- colorVar = {
335
- value: { alias: { guid: deepClone(variable.guid) } },
336
- dataType: 'ALIAS',
337
- resolvedDataType: 'COLOR',
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 ?? { r: 0, g: 0, b: 0 };
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 ?? { r: 0, g: 0, b: 0 };
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: color.r, g: color.g, b: color.b, a: color.a ?? 1 },
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: color.r, g: color.g, b: color.b, a: color.a ?? 1 },
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 a named color (e.g. 'Blue', 'Red') to its VARIABLE node.
1626
- * Returns { guid, r, g, b } from the Light Slides color variable set.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figmatk",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Figma Toolkit — Swiss-army knife CLI for Figma .deck and .fig files",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,39 +1,204 @@
1
1
  ---
2
2
  name: figmatk
3
3
  description: >
4
- Inspect and modify Figma Slides .deck files. Use when the user asks to
5
- "edit a deck", "modify a presentation", "inspect slides", "update slide text",
6
- "insert an image into a slide", "clone a slide", "remove a slide",
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.3"
8
+ version: "0.0.7"
10
9
  ---
11
10
 
12
- Use the FigmaTK MCP tools to manipulate Figma .deck files. The tools are prefixed with `figmatk_`.
11
+ # FigmaTK Skill
13
12
 
14
- ## Available tools
13
+ ## Quick Reference
15
14
 
16
- - `figmatk_inspect` Show the node hierarchy tree
17
- - `figmatk_list_text` — List all text and image content per slide
18
- - `figmatk_list_overrides` List editable override keys per symbol
19
- - `figmatk_update_text` Apply text overrides to a slide instance
20
- - `figmatk_insert_image` Apply image fill override with hash + thumbnail
21
- - `figmatk_clone_slide` Duplicate a slide
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
- ## Workflow
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
- 1. Start with `figmatk_inspect` to understand the deck structure
28
- 2. Use `figmatk_list_text` to see current content
29
- 3. Use `figmatk_list_overrides` to find editable keys
30
- 4. Apply changes with `figmatk_update_text` or `figmatk_insert_image`
31
- 5. Always write to a new output path never overwrite the source
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 rules
198
+ ## Critical Format Rules
34
199
 
35
- - Text overrides use `overrideKey: text` pairs where keys are `"sessionID:localID"` format
36
- - Blank text fields must be a single space `" "`, never empty string (crashes Figma)
37
- - Image overrides require both a full image hash and a thumbnail hash (40-char hex SHA-1)
38
- - Removed nodes use `phase: 'REMOVED'` never delete from nodeChanges
39
- - Always roundtrip-test after modifications to validate the pipeline
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 `{}`