figmatk 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "name": "figmatk",
15
15
  "description": "Swiss Army Knife for Figma Files (.deck)",
16
- "version": "0.1.1",
16
+ "version": "0.2.1",
17
17
  "author": {
18
18
  "name": "FigmaTK Contributors"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figmatk",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Create and edit Figma Slides .deck files programmatically — no Figma API required",
5
5
  "author": {
6
6
  "name": "FigmaTK Contributors"
package/lib/api.mjs CHANGED
@@ -561,7 +561,7 @@ export class Slide {
561
561
  addRectangle(x, y, width, height, opts = {}) {
562
562
  const fd = this._fd;
563
563
  const localID = fd.maxLocalID() + 1;
564
- const fill = opts.fill ?? { r: 1, g: 1, b: 1, a: 1 };
564
+ const fill = parseColor(fd, opts.fill ?? 'White');
565
565
 
566
566
  const node = {
567
567
  guid: { sessionID: 1, localID },
@@ -904,7 +904,7 @@ export class Slide {
904
904
  _addShapeWithText(shapeType, x, y, width, height, opts = {}) {
905
905
  const fd = this._fd;
906
906
  const localID = fd.maxLocalID() + 1;
907
- const fill = opts.fill ?? { r: 1, g: 1, b: 1, a: 1 };
907
+ const fill = parseColor(fd, opts.fill ?? 'White');
908
908
  const fillPaint = {
909
909
  type: 'SOLID',
910
910
  color: { r: fill.r, g: fill.g, b: fill.b, a: fill.a ?? 1 },
@@ -1185,7 +1185,7 @@ export class Slide {
1185
1185
  fd.message.blobs.push({ bytes: _buildVectorNetworkBlob(allCmds) });
1186
1186
  const vnbIdx = fd.message.blobs.length - 1;
1187
1187
 
1188
- const fill = opts.fill ?? { r: 0, g: 0, b: 0 };
1188
+ const fill = parseColor(fd, opts.fill ?? 'Black');
1189
1189
  const opacity = opts.opacity ?? 1;
1190
1190
 
1191
1191
  const frameId = nextId++;
package/mcp-server.mjs CHANGED
@@ -6,6 +6,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
7
  import { z } from 'zod';
8
8
  import { FigDeck } from './lib/fig-deck.mjs';
9
+ import { Deck } from './lib/api.mjs';
9
10
  import { nid, ov, nestedOv, removeNode, parseId, positionChar } from './lib/node-helpers.mjs';
10
11
  import { imageOv, hexToHash, hashToHex } from './lib/image-helpers.mjs';
11
12
  import { deepClone } from './lib/deep-clone.mjs';
@@ -246,6 +247,95 @@ server.tool(
246
247
  }
247
248
  );
248
249
 
250
+ // ── create-deck ─────────────────────────────────────────────────────────
251
+ const THEMES = {
252
+ midnight: { dark: 'Black', light: 'White', accent: { r: 0.792, g: 0.863, b: 0.988 }, textDark: 'White', textLight: 'Black' },
253
+ ocean: { dark: 'Blue', light: 'Pale Blue', accent: { r: 0.129, g: 0.161, b: 0.361 }, textDark: 'White', textLight: 'Black' },
254
+ forest: { dark: 'Green', light: 'Pale Green', accent: { r: 0.592, g: 0.737, b: 0.384 }, textDark: 'White', textLight: 'Black' },
255
+ coral: { dark: 'Persimmon', light: 'Pale Persimmon', accent: { r: 0.184, g: 0.235, b: 0.494 }, textDark: 'White', textLight: 'Black' },
256
+ terracotta: { dark: 'Persimmon', light: 'Pale Persimmon', accent: { r: 0.906, g: 0.910, b: 0.820 }, textDark: 'White', textLight: 'Black' },
257
+ minimal: { dark: 'Black', light: 'White', accent: { r: 0.212, g: 0.271, b: 0.310 }, textDark: 'White', textLight: 'Black' },
258
+ };
259
+
260
+ const SlideSchema = z.object({
261
+ type: z.enum(['title', 'bullets', 'two-column', 'stat', 'image-full', 'closing']),
262
+ title: z.string().optional(),
263
+ subtitle: z.string().optional(),
264
+ body: z.string().optional(),
265
+ bullets: z.array(z.string()).optional(),
266
+ stat: z.string().optional(),
267
+ caption: z.string().optional(),
268
+ image: z.string().optional().describe('Absolute path to image file'),
269
+ leftText: z.string().optional(),
270
+ rightText: z.string().optional(),
271
+ background: z.string().optional().describe('Override background (named color)'),
272
+ });
273
+
274
+ server.tool(
275
+ 'figmatk_create_deck',
276
+ 'Create a new Figma Slides .deck file from a structured description. No npm install needed — runs directly in the MCP server.',
277
+ {
278
+ output: z.string().describe('Output path for the .deck file, e.g. /tmp/my-deck.deck'),
279
+ title: z.string().describe('Deck title'),
280
+ theme: z.string().optional().describe('Theme: midnight | ocean | forest | coral | terracotta | minimal (default: midnight)'),
281
+ slides: z.array(SlideSchema).describe('Slides to create'),
282
+ },
283
+ async ({ output, title, theme, slides }) => {
284
+ const t = THEMES[theme ?? 'midnight'] ?? THEMES.midnight;
285
+ const deck = await Deck.create(title);
286
+
287
+ for (const s of slides) {
288
+ const slide = deck.addBlankSlide();
289
+ const isDark = ['title', 'closing', 'stat', 'image-full'].includes(s.type);
290
+ const bg = s.background ?? (isDark ? t.dark : t.light);
291
+ const fg = isDark ? t.textDark : t.textLight;
292
+ slide.setBackground(bg);
293
+
294
+ if (s.type === 'title' || s.type === 'closing') {
295
+ slide.addRectangle(0, 0, 1920, 8, { fill: t.accent });
296
+ if (s.title) slide.addText(s.title, { style: 'Title', color: fg, x: 80, y: 360, width: 1760, align: 'CENTER' });
297
+ if (s.subtitle) slide.addText(s.subtitle, { style: 'Body 1', color: fg, x: 80, y: 540, width: 1760, align: 'CENTER' });
298
+
299
+ } else if (s.type === 'bullets') {
300
+ slide.addRectangle(0, 0, 1920, 8, { fill: t.accent });
301
+ if (s.title) slide.addText(s.title, { style: 'Header 2', color: fg, x: 80, y: 80, width: 1760, align: 'LEFT' });
302
+ const items = s.bullets ?? (s.body ? s.body.split('\n') : []);
303
+ let y = 240;
304
+ for (const item of items) {
305
+ slide.addRectangle(80, y + 10, 12, 12, { fill: t.accent });
306
+ slide.addText(item, { style: 'Body 1', color: fg, x: 116, y, width: 1724, align: 'LEFT' });
307
+ y += 80;
308
+ }
309
+
310
+ } else if (s.type === 'two-column') {
311
+ slide.addRectangle(0, 0, 1920, 8, { fill: t.accent });
312
+ if (s.title) slide.addText(s.title, { style: 'Header 2', color: fg, x: 80, y: 80, width: 1760, align: 'LEFT' });
313
+ slide.addRectangle(960, 200, 4, 800, { fill: t.accent });
314
+ if (s.leftText) slide.addText(s.leftText, { style: 'Body 1', color: fg, x: 80, y: 240, width: 840, align: 'LEFT' });
315
+ if (s.rightText) slide.addText(s.rightText, { style: 'Body 1', color: fg, x: 1004, y: 240, width: 836, align: 'LEFT' });
316
+ if (s.image) await slide.addImage(s.image, { x: 1004, y: 200, width: 836, height: 800 });
317
+
318
+ } else if (s.type === 'stat') {
319
+ slide.addRectangle(0, 0, 1920, 8, { fill: t.accent });
320
+ if (s.title) slide.addText(s.title, { style: 'Header 2', color: fg, x: 80, y: 80, width: 1760, align: 'LEFT' });
321
+ if (s.stat) slide.addText(s.stat, { style: 'Title', color: fg, x: 80, y: 300, width: 1760, align: 'CENTER' });
322
+ if (s.caption) slide.addText(s.caption, { style: 'Body 1', color: fg, x: 80, y: 720, width: 1760, align: 'CENTER' });
323
+
324
+ } else if (s.type === 'image-full') {
325
+ if (s.image) await slide.addImage(s.image, { x: 0, y: 0, width: 1920, height: 1080 });
326
+ if (s.title || s.body) {
327
+ slide.addRectangle(0, 680, 1920, 400, { fill: { r: 0, g: 0, b: 0 }, opacity: 0.7 });
328
+ if (s.title) slide.addText(s.title, { style: 'Header 1', color: 'White', x: 80, y: 720, width: 1760, align: 'LEFT' });
329
+ if (s.body) slide.addText(s.body, { style: 'Body 1', color: 'White', x: 80, y: 880, width: 1760, align: 'LEFT' });
330
+ }
331
+ }
332
+ }
333
+
334
+ await deck.save(output);
335
+ return { content: [{ type: 'text', text: `Created ${output} — ${slides.length} slides. Open in Figma Desktop.` }] };
336
+ }
337
+ );
338
+
249
339
  // ── Start server ────────────────────────────────────────────────────────
250
340
  const transport = new StdioServerTransport();
251
341
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figmatk",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Figma Toolkit — Swiss-army knife CLI for Figma .deck and .fig files",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,7 +6,7 @@ description: >
6
6
  clone or remove slides, or produce a .deck file for Figma Slides.
7
7
  Powered by FigmaTK under the hood.
8
8
  metadata:
9
- version: "0.1.1"
9
+ version: "0.2.1"
10
10
  ---
11
11
 
12
12
  # Figma Slides Creator
@@ -23,10 +23,10 @@ To let the user view the result: tell them to **open the file in Figma Desktop**
23
23
 
24
24
  | Task | Approach |
25
25
  |------|----------|
26
- | Create a new deck from scratch | Use the high-level JS API (`lib/api.mjs`) |
27
- | Edit text or images in an existing deck | Use MCP tools (`figmatk_update_text`, `figmatk_insert_image`) |
28
- | Clone, remove, or restructure slides | Use MCP tools (`figmatk_clone_slide`, `figmatk_remove_slide`) |
29
- | Inspect structure or read content | Use MCP tools (`figmatk_inspect`, `figmatk_list_text`) |
26
+ | Create a new deck from scratch | **`figmatk_create_deck` MCP tool** no npm install needed |
27
+ | Edit text or images in an existing deck | `figmatk_update_text`, `figmatk_insert_image` |
28
+ | Clone, remove, or restructure slides | `figmatk_clone_slide`, `figmatk_remove_slide` |
29
+ | Inspect structure or read content | `figmatk_inspect`, `figmatk_list_text` |
30
30
 
31
31
  ---
32
32
 
@@ -36,25 +36,60 @@ To let the user view the result: tell them to **open the file in Figma Desktop**
36
36
 
37
37
  ---
38
38
 
39
- ## Path A — Create from Scratch (High-Level API)
39
+ ## Path A — Create from Scratch (MCP tool — preferred)
40
+
41
+ **Always use this path.** No npm install, no scripts, no workspace setup.
42
+
43
+ Call `figmatk_create_deck` with a structured slide description:
44
+
45
+ ```json
46
+ {
47
+ "output": "/tmp/my-deck.deck",
48
+ "title": "My Presentation",
49
+ "theme": "midnight",
50
+ "slides": [
51
+ { "type": "title", "title": "My Presentation", "subtitle": "A subtitle" },
52
+ { "type": "bullets", "title": "Key Points", "bullets": ["Point one", "Point two", "Point three"] },
53
+ { "type": "two-column", "title": "Comparison", "leftText": "Left side content", "rightText": "Right side content" },
54
+ { "type": "stat", "title": "By the numbers", "stat": "42%", "caption": "of users prefer this" },
55
+ { "type": "image-full", "image": "/tmp/photo.jpg", "title": "Caption text" },
56
+ { "type": "closing", "title": "Thank you", "subtitle": "Questions?" }
57
+ ]
58
+ }
59
+ ```
60
+
61
+ ### Slide types
62
+
63
+ | Type | Fields |
64
+ |------|--------|
65
+ | `title` | `title`, `subtitle` |
66
+ | `bullets` | `title`, `bullets` (array) |
67
+ | `two-column` | `title`, `leftText`, `rightText`, `image` (right side) |
68
+ | `stat` | `title`, `stat` (big number), `caption` |
69
+ | `image-full` | `image` (path), `title`, `body` (overlay text) |
70
+ | `closing` | `title`, `subtitle` |
71
+
72
+ ### Themes
73
+
74
+ `midnight` · `ocean` · `forest` · `coral` · `terracotta` · `minimal`
75
+
76
+ Each theme handles backgrounds, accent colors, and text colors automatically.
40
77
 
41
- Use this when the user wants a new presentation. Follow these steps **in order, every time, no exceptions**.
78
+ ---
79
+
80
+ ## Path A2 — Create from Scratch (Node.js script fallback)
42
81
 
43
- ### Step 1 Set up workspace (MANDATORY FIRST STEP never skip)
82
+ Only use this if `figmatk_create_deck` is unavailable or you need layout control beyond what the MCP tool offers.
44
83
 
45
- The environment has no `node_modules`. **Before writing any script**, run this exact command:
84
+ ### Step 1 Set up workspace (MANDATORY never skip)
46
85
 
47
86
  ```bash
48
87
  [ -d /tmp/figmatk-ws/node_modules ] || (mkdir -p /tmp/figmatk-ws && cd /tmp/figmatk-ws && npm init -y && npm install figmatk)
49
88
  ```
50
89
 
51
- Do not proceed to Step 2 until this command succeeds.
52
-
53
- ### Step 2 — Write the script to `/tmp/figmatk-ws/deck.mjs`
54
-
55
- **Always write the script to `/tmp/figmatk-ws/deck.mjs`** — not to the current directory, not to any other path.
90
+ ### Step 2 Write script to `/tmp/figmatk-ws/deck.mjs`
56
91
 
57
- **Always use the bare specifier** `import { Deck } from 'figmatk'` — never a file path import.
92
+ **Always use bare specifier** `import { Deck } from 'figmatk'` — never a file path.
58
93
 
59
94
  ```javascript
60
95
  import { Deck } from 'figmatk';
@@ -64,32 +99,19 @@ function hex(h) {
64
99
  }
65
100
 
66
101
  const deck = await Deck.create('My Presentation');
67
-
68
- // Each call to addBlankSlide() returns a new blank slide.
69
- // The template blank slide is auto-removed on the first call.
70
102
  const slide = deck.addBlankSlide();
71
- slide.setBackground('Black'); // named color — see list below
72
- slide.addText('Slide Title', {
73
- style: 'Title', color: 'White',
74
- x: 64, y: 80, width: 1792, align: 'LEFT'
75
- });
76
- slide.addText('Subtitle', {
77
- style: 'Body 1', color: 'Grey',
78
- x: 64, y: 240, width: 1200, align: 'LEFT'
79
- });
80
-
103
+ slide.setBackground('Black');
104
+ slide.addText('Slide Title', { style: 'Title', color: 'White', x: 64, y: 80, width: 1792, align: 'LEFT' });
81
105
  await deck.save('/tmp/my-presentation.deck');
82
- console.log('Done — open /tmp/my-presentation.deck in Figma Desktop');
106
+ console.log('Done');
83
107
  ```
84
108
 
85
- ### Step 3 — Run the script
109
+ ### Step 3 — Run
86
110
 
87
111
  ```bash
88
112
  node /tmp/figmatk-ws/deck.mjs
89
113
  ```
90
114
 
91
- If this fails, check the error and fix the script — **do not change the workspace setup or the import path**.
92
-
93
115
  ### ⚠️ Critical gotchas
94
116
 
95
117
  | Issue | Wrong | Right |
@@ -97,7 +119,7 @@ If this fails, check the error and fix the script — **do not change the worksp
97
119
  | `setBackground` with hex | `s.setBackground('#1A1A1A')` | `s.setBackground('Black')` |
98
120
  | `setBackground` with raw RGB | `s.setBackground({ r:0.1, g:0.1, b:0.1 })` | `s.setBackground('Black')` — raw RGB silently renders white |
99
121
  | Shape method signature | `s.addRectangle({ x:0, y:0, width:100 })` | `s.addRectangle(0, 0, 100, 100, opts)` |
100
- | Shape fill color | `{ fill: '#F4900C' }` | `{ fill: hex('#F4900C') }` — use the hex() helper |
122
+ | Shape fill color | `{ fill: { r:1, g:0, b:0 } }` | `{ fill: '#F4900C' }` or `{ fill: 'Red' }` — hex strings and named colors work directly |
101
123
  | `addLine` options | `{ strokeColor: ..., strokeWeight: 2 }` | `{ color: 'Black', weight: 2 }` |
102
124
  | `align` value | `align: 'left'` | `align: 'LEFT'` (uppercase) |
103
125
  | `addImage` without await | `slide.addImage('/tmp/photo.jpg')` | `await slide.addImage('/tmp/photo.jpg')` — async, images silently missing without await |
@@ -179,6 +201,7 @@ Use this when the user provides a `.deck` file to modify.
179
201
 
180
202
  | Tool | Purpose |
181
203
  |------|---------|
204
+ | `figmatk_create_deck` | **Create a new deck from scratch** — no npm install needed |
182
205
  | `figmatk_inspect` | Node hierarchy tree — structure, node IDs, slide count |
183
206
  | `figmatk_list_text` | All text strings and image hashes per slide |
184
207
  | `figmatk_list_overrides` | Editable override keys per symbol (component) |