figmatk 0.0.6 → 0.0.7

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,56 @@ 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
+ // Designer alias (case-insensitive)
1671
+ const alias = DESIGNER_COLORS[color.toLowerCase()];
1672
+ if (alias) return _hexToRgb(alias);
1673
+ // Figma theme variable
1674
+ const variable = resolveColorVariable(fd, color);
1675
+ return { r: variable.r, g: variable.g, b: variable.b, _guid: variable.guid };
1676
+ }
1677
+ throw new Error(`Invalid color: ${JSON.stringify(color)}`);
1678
+ }
1679
+
1628
1680
  /**
1629
1681
  * Resolve a named color (e.g. 'Blue', 'Red') to its VARIABLE node.
1630
1682
  * 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.7",
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,188 @@
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.6"
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
+ ```javascript
29
+ import { Deck } from 'figmatk';
30
+
31
+ const deck = await Deck.create('My Presentation');
32
+ const slide = deck.addBlankSlide();
33
+
34
+ slide.setBackground('slate');
35
+ slide.addText('Slide Title', { style: 'Title', color: 'white', x: 64, y: 80, width: 1792 });
36
+ slide.addText('Subtitle or tagline here', { style: 'Body 1', color: 'light-gray', x: 64, y: 240, width: 1200 });
37
+
38
+ await deck.save('/path/to/output.deck');
39
+ ```
40
+
41
+ ### Text styles
42
+
43
+ | Style | Size | Weight | Use for |
44
+ |-------|------|--------|---------|
45
+ | `Title` | 96pt | Bold | Slide title |
46
+ | `Header 1` | 60pt | Bold | Section headers |
47
+ | `Header 2` | 48pt | Bold | Sub-headers |
48
+ | `Header 3` | 36pt | Bold | In-slide headings |
49
+ | `Body 1` | 36pt | Regular | Primary body text |
50
+ | `Body 2` | 30pt | Regular | Secondary body text |
51
+ | `Body 3` | 24pt | Regular | Captions, labels |
52
+ | `Note` | 20pt | Regular | Footnotes, sources |
53
+
54
+ ### Colors
55
+
56
+ Use **names** — never RGB values. The API accepts:
57
+
58
+ | Type | Example |
59
+ |------|---------|
60
+ | Designer name | `'teal'`, `'coral'`, `'navy'`, `'midnight'`, `'forest'`, `'charcoal'`, `'cream'`, `'gold'`, `'terracotta'`, `'sage'`, `'cobalt'`, `'rose'`, `'indigo'`, `'burgundy'`, `'sand'`... |
61
+ | Hex string | `'#E63946'` |
62
+ | Light Slides theme | `'Blue'`, `'Red'`, `'Green'`, `'Yellow'`, `'Orange'`, `'Pink'`, `'Purple'`, `'Slate'`, `'White'`, `'Black'` |
63
+
64
+ Pick colors by feeling: "warm terracotta on cream", "midnight blue with coral accent". Let the design drive the choice, then pick the closest name.
65
+
66
+ ### Slide dimensions
67
+
68
+ 1920 × 1080px. Position and size all elements in pixels.
69
+
70
+ ### Available slide methods
71
+
72
+ ```javascript
73
+ slide.setBackground(color) // named color or hex
74
+ slide.addText(text, opts) // opts: style, color, x, y, width, align, bold, italic, fontSize
75
+ slide.addFrame(opts) // auto-layout frame: stackMode, spacing, x, y, width, height
76
+ slide.addRectangle(opts) // opts: x, y, width, height, fill, opacity, cornerRadius
77
+ slide.addEllipse(opts) // circle/ellipse: x, y, width, height, fill
78
+ slide.addDiamond(opts) // diamond shape
79
+ slide.addTriangle(opts) // triangle
80
+ slide.addStar(opts) // 5-pointed star
81
+ slide.addLine(x1, y1, x2, y2, opts) // line: strokeColor, strokeWeight
82
+ slide.addImage(path, opts) // freestanding image: x, y, width, height
83
+ slide.addTable(data, opts) // 2D array of strings: x, y, width, colWidths, rowHeight
84
+ slide.addSVG(x, y, width, svgPathOrBuf, opts) // import SVG vector graphic
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Path B — Edit an Existing Deck (MCP Tools)
90
+
91
+ Use this when the user provides a `.deck` file to modify.
92
+
93
+ ### Workflow
94
+
95
+ 1. `figmatk_inspect` — understand the deck structure (node IDs, slide count, symbols)
96
+ 2. `figmatk_list_text` — read current text and images per slide
97
+ 3. `figmatk_list_overrides` — find the override keys for each symbol (what's editable)
98
+ 4. `figmatk_update_text` — apply text changes
99
+ 5. `figmatk_insert_image` — apply image changes
100
+ 6. `figmatk_clone_slide` — duplicate a slide and populate it
101
+ 7. `figmatk_remove_slide` — mark unwanted slides as REMOVED
102
+ 8. Always write to a **new output path** — never overwrite the source
103
+
104
+ ### MCP tool reference
26
105
 
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
106
+ | Tool | Purpose |
107
+ |------|---------|
108
+ | `figmatk_inspect` | Node hierarchy tree — structure, node IDs, slide count |
109
+ | `figmatk_list_text` | All text strings and image hashes per slide |
110
+ | `figmatk_list_overrides` | Editable override keys per symbol (component) |
111
+ | `figmatk_update_text` | Set text overrides on a slide instance |
112
+ | `figmatk_insert_image` | Set image fill override (handles SHA-1 hashing + thumbnail) |
113
+ | `figmatk_clone_slide` | Deep-clone a slide with new text and images |
114
+ | `figmatk_remove_slide` | Mark slides as REMOVED (never deleted) |
115
+ | `figmatk_roundtrip` | Decode + re-encode for pipeline validation |
116
+
117
+ ---
118
+
119
+ ## Design Philosophy
120
+
121
+ Every deck must look **intentionally designed**, not AI-generated. Follow these rules on every presentation task.
122
+
123
+ ### Colour
124
+
125
+ - Pick a bold palette that reflects the **specific topic**. If the same palette would suit a completely different presentation, it's not specific enough.
126
+ - Use **one dominant colour** (60–70% visual weight) + 1–2 supporting tones + one sharp accent.
127
+ - Use dark backgrounds on title and conclusion slides, light on content slides ("sandwich" structure) — or commit fully to dark for a premium feel.
128
+
129
+ **Starter palettes:**
130
+
131
+ | Theme | Background | Accent | Text |
132
+ |-------|-----------|--------|------|
133
+ | Midnight | `navy` | `sky` | `white` |
134
+ | Forest | `forest` | `sage` | `white` |
135
+ | Coral | `coral` | `indigo` | `white` |
136
+ | Terracotta | `terracotta` | `sand` | `white` |
137
+ | Ocean | `cobalt` | `midnight` | `white` |
138
+ | Minimal | `smoke` | `charcoal` | `black` |
139
+
140
+ ### Layout
141
+
142
+ - Every slide needs at least **one visual element** — shape, image, SVG icon, or table. Text-only slides are forgettable.
143
+ - **Vary layouts** — never use the same structure slide after slide.
144
+ - Pick one visual motif (e.g. rounded image frames, coloured icon circles, thick side accent bars) and carry it through every slide.
145
+
146
+ **Layout options per slide:**
147
+
148
+ - Two-column (text left, visual right)
149
+ - Icon + text rows (icon in coloured circle, bold header, description)
150
+ - 2×2 or 2×3 grid of cards
151
+ - Large stat callout (big number + small label)
152
+ - Half-background image with text overlay
153
+ - Timeline / numbered steps
154
+
155
+ ### Typography
156
+
157
+ - Left-align body text. Centre only titles.
158
+ - **Font sizes:** titles use `Title` style (96pt); section headers `Header 1` (60pt); body `Body 1` or `Body 2`; captions `Body 3` or `Note`.
159
+ - Minimum 64px margin from slide edges. 24–48px gap between content blocks.
160
+
161
+ ### Things to never do
162
+
163
+ - Repeat the same layout slide after slide
164
+ - Centre body text
165
+ - Use accent lines under slide titles (hallmark of AI-generated slides — use colour or whitespace instead)
166
+ - Create text-only slides
167
+ - Use low-contrast text — always check text against its background
168
+ - Default to generic blue — pick colours that reflect the topic
169
+
170
+ ---
171
+
172
+ ## QA
173
+
174
+ After generating or editing a deck:
175
+
176
+ 1. **Self-check:** confirm all placeholder text has been replaced, no `lorem ipsum` or `[title here]` remains
177
+ 2. **Tell the user** to upload the `.deck` to Figma Slides and review visually — this is the only way to catch rendering issues
178
+ 3. **Offer to fix** any issues the user reports after upload
179
+
180
+ ---
32
181
 
33
- ## Critical rules
182
+ ## Critical Format Rules
34
183
 
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
184
+ - Blank text fields must use `" "` (space), **never** empty string empty string crashes Figma
185
+ - Image overrides require both a full-image hash and a ~320px thumbnail hash (40-char hex SHA-1)
186
+ - Removed nodes use `phase: 'REMOVED'` never delete from `nodeChanges`
187
+ - Chunk 1 of `canvas.fig` must be zstd-compressed Figma silently rejects deflateRaw
188
+ - `thumbHash` must be `new Uint8Array(0)`, never `{}`