figmatk 0.3.1 → 0.3.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.
Files changed (35) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +39 -21
  4. package/cli.mjs +2 -0
  5. package/commands/render.mjs +56 -0
  6. package/lib/rasterizer/deck-rasterizer.mjs +228 -0
  7. package/lib/rasterizer/download-font.mjs +57 -0
  8. package/lib/rasterizer/font-resolver.mjs +602 -0
  9. package/lib/rasterizer/fonts/DarkerGrotesque-400.ttf +0 -0
  10. package/lib/rasterizer/fonts/DarkerGrotesque-500.ttf +0 -0
  11. package/lib/rasterizer/fonts/DarkerGrotesque-600.ttf +0 -0
  12. package/lib/rasterizer/fonts/Inter-Regular.ttf +0 -0
  13. package/lib/rasterizer/fonts/Inter-Variable.ttf +0 -0
  14. package/lib/rasterizer/fonts/Inter.var.woff2 +0 -0
  15. package/lib/rasterizer/fonts/darker-grotesque-patched-400-normal.woff2 +0 -0
  16. package/lib/rasterizer/fonts/darker-grotesque-patched-500-normal.woff2 +0 -0
  17. package/lib/rasterizer/fonts/darker-grotesque-patched-600-normal.woff2 +0 -0
  18. package/lib/rasterizer/fonts/darker-grotesque-patched-700-normal.woff2 +0 -0
  19. package/lib/rasterizer/fonts/inter-v3-400-italic.woff2 +0 -0
  20. package/lib/rasterizer/fonts/inter-v3-400-normal.woff2 +0 -0
  21. package/lib/rasterizer/fonts/inter-v3-500-normal.woff2 +0 -0
  22. package/lib/rasterizer/fonts/inter-v3-600-normal.woff2 +0 -0
  23. package/lib/rasterizer/fonts/inter-v3-700-italic.woff2 +0 -0
  24. package/lib/rasterizer/fonts/inter-v3-700-normal.woff2 +0 -0
  25. package/lib/rasterizer/fonts/inter-v3-meta.json +6 -0
  26. package/lib/rasterizer/render-report-lib.mjs +127 -0
  27. package/lib/rasterizer/render-report.mjs +25 -0
  28. package/lib/rasterizer/svg-builder.mjs +626 -0
  29. package/lib/rasterizer/test-render.mjs +63 -0
  30. package/lib/template-deck.mjs +29 -1
  31. package/manifest.json +21 -0
  32. package/mcp-server.mjs +65 -4
  33. package/package.json +17 -2
  34. package/skills/figma-slides-creator/SKILL.md +82 -209
  35. package/skills/figma-template-builder/SKILL.md +11 -1
@@ -298,13 +298,41 @@ function describeLayout(deck, layout, row) {
298
298
  };
299
299
  }
300
300
 
301
+ /**
302
+ * Walk the node tree like deck.walkTree, but follow INSTANCE → SYMBOL links.
303
+ * When an INSTANCE node is encountered, its referenced SYMBOL's children are
304
+ * also walked so that slots inside published template components are discovered.
305
+ */
306
+ function walkTreeThroughInstances(deck, rootId, visitor, depth = 0, visited = new Set()) {
307
+ if (!rootId || visited.has(rootId)) return;
308
+ visited.add(rootId);
309
+
310
+ const node = deck.getNode(rootId);
311
+ if (!node || node.phase === 'REMOVED') return;
312
+ visitor(node, depth);
313
+
314
+ // Follow INSTANCE → SYMBOL: walk the SYMBOL's children
315
+ if (node.type === 'INSTANCE' && node.symbolData?.symbolID) {
316
+ const sid = node.symbolData.symbolID;
317
+ const symNid = `${sid.sessionID}:${sid.localID}`;
318
+ for (const child of deck.getChildren(symNid)) {
319
+ walkTreeThroughInstances(deck, nid(child), visitor, depth + 1, visited);
320
+ }
321
+ }
322
+
323
+ // Walk direct children
324
+ for (const child of deck.getChildren(rootId)) {
325
+ walkTreeThroughInstances(deck, nid(child), visitor, depth + 1, visited);
326
+ }
327
+ }
328
+
301
329
  function discoverSlots(deck, rootId) {
302
330
  const explicitTextSlots = [];
303
331
  const explicitImageSlots = [];
304
332
  const fallbackTextSlots = [];
305
333
  const fallbackImageSlots = [];
306
334
 
307
- deck.walkTree(rootId, node => {
335
+ walkTreeThroughInstances(deck, rootId, node => {
308
336
  const textSlotName = parsePrefixedName(node.name, TEXT_SLOT_PREFIX);
309
337
  if (textSlotName) {
310
338
  const slot = describeTextSlot(node, textSlotName, 'explicit');
package/manifest.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "manifest_version": "0.2",
3
+ "name": "figmatk",
4
+ "version": "0.3.3",
5
+ "description": "Create and edit Figma Slides .deck files programmatically - no Figma API required",
6
+ "author": {
7
+ "name": "FigmaTK Contributors"
8
+ },
9
+ "server": {
10
+ "type": "node",
11
+ "entry_point": "mcp-server.mjs",
12
+ "mcp_config": {
13
+ "command": "node",
14
+ "args": [
15
+ "${__dirname}/mcp-server.mjs"
16
+ ],
17
+ "env": {}
18
+ }
19
+ },
20
+ "license": "MIT"
21
+ }
package/mcp-server.mjs CHANGED
@@ -5,8 +5,12 @@
5
5
  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
+ import packageJson from './package.json' with { type: 'json' };
8
9
  import { FigDeck } from './lib/fig-deck.mjs';
9
10
  import { Deck } from './lib/api.mjs';
11
+ import { slideToSvg } from './lib/rasterizer/svg-builder.mjs';
12
+ import { svgToPng } from './lib/rasterizer/deck-rasterizer.mjs';
13
+ import { resolveFonts } from './lib/rasterizer/font-resolver.mjs';
10
14
  import {
11
15
  annotateTemplateLayout,
12
16
  createDraftTemplate,
@@ -20,7 +24,7 @@ import { deepClone } from './lib/deep-clone.mjs';
20
24
 
21
25
  const server = new McpServer({
22
26
  name: 'figmatk',
23
- version: '0.0.3',
27
+ version: packageJson.version,
24
28
  });
25
29
 
26
30
  // ── inspect ─────────────────────────────────────────────────────────────
@@ -435,7 +439,7 @@ server.tool(
435
439
 
436
440
  server.tool(
437
441
  'figmatk_list_template_layouts',
438
- 'Inspect a template or draft template .deck file and return available layouts with explicit text/image slot metadata. Call this before figmatk_create_from_template or figmatk_annotate_template_layout.',
442
+ 'Inspect a template or draft template .deck file and return a layout library catalog with explicit text/image slot metadata. Use this to inventory candidate layouts, classify what each layout is for, and choose a subset before calling figmatk_create_from_template. Do not edit, remove, or reorder the source template as a way to build the output deck.',
439
443
  {
440
444
  template: z.string().describe('Path to the .deck template file'),
441
445
  },
@@ -459,7 +463,7 @@ server.tool(
459
463
  // ── figmatk_create_from_template ─────────────────────────────────────────
460
464
  server.tool(
461
465
  'figmatk_create_from_template',
462
- 'Create a new Figma Slides deck by cherry-picking layouts from a draft, published, or publish-like template .deck file and populating explicit text/image slots. Preserves colors, fonts, internal assets, and special nodes.',
466
+ 'Create a new Figma Slides deck by selecting any ordered subset of layouts from a draft, published, or publish-like template .deck file and populating explicit text/image slots. The slides array defines the output order. This clones the chosen layouts into a new deck; do not remove, reorder, or mutate the source template to build the result. Works with flat-frame template slides as well as module-backed layouts, and preserves colors, fonts, internal assets, and special nodes.',
463
467
  {
464
468
  template: z.string().describe('Path to the source .deck template file'),
465
469
  output: z.string().describe('Output path for the new .deck file (use /tmp/)'),
@@ -467,7 +471,7 @@ server.tool(
467
471
  slideId: z.string().describe('Slide ID from figmatk_list_template_layouts (e.g. "1:74")'),
468
472
  text: z.record(z.string()).optional().describe('Map of text slot/name/nodeId -> value (e.g. { "title": "My Company" })'),
469
473
  images: z.record(z.string()).optional().describe('Map of image slot/name/nodeId -> absolute image path (e.g. { "hero_image": "/tmp/photo.jpg" })'),
470
- })).describe('Ordered list of slides to include, each referencing a template layout'),
474
+ })).describe('Ordered subset of template layouts to include in the output deck. The array order becomes the output deck order, regardless of the template\'s original order.'),
471
475
  },
472
476
  async ({ template, output, slides }) => {
473
477
  const bytes = await createFromTemplate(template, output, slides);
@@ -475,6 +479,63 @@ server.tool(
475
479
  }
476
480
  );
477
481
 
482
+ // ── render-slide ───────────────────────────────────────────────────────
483
+ server.tool(
484
+ 'figmatk_render_slide',
485
+ 'Render a slide from a .deck file to an image. Without output path, returns inline WebP for visual QA. With output path, saves full PNG. Default is 1920×1080; use width (pixels) or scale (e.g. "50%") to resize.',
486
+ {
487
+ path: z.string().describe('Path to .deck file'),
488
+ slide: z.number().int().min(1).describe('Slide number (1-based)'),
489
+ output: z.string().optional().describe('Optional output path to save the PNG file (full resolution)'),
490
+ width: z.number().int().optional().describe('Output width in pixels (height scales proportionally)'),
491
+ scale: z.string().optional().describe('Scale as percentage, e.g. "50%" or "25%"'),
492
+ },
493
+ async ({ path, slide, output, width, scale }) => {
494
+ const deck = await FigDeck.fromDeckFile(path);
495
+ await resolveFonts(deck, { quiet: true });
496
+ const slides = deck.getActiveSlides();
497
+
498
+ if (slide > slides.length) {
499
+ return { content: [{ type: 'text', text: `Slide ${slide} does not exist — deck has ${slides.length} slides` }] };
500
+ }
501
+
502
+ const svg = slideToSvg(deck, slides[slide - 1]);
503
+
504
+ // Build render options
505
+ const renderOpts = {};
506
+ if (width) {
507
+ renderOpts.width = width;
508
+ } else if (scale) {
509
+ const pct = parseFloat(scale.replace('%', ''));
510
+ if (!isNaN(pct) && pct > 0) renderOpts.scale = pct / 100;
511
+ }
512
+
513
+ const png = await svgToPng(svg, renderOpts);
514
+ const buf = Buffer.from(png);
515
+
516
+ if (output) {
517
+ const { writeFileSync } = await import('fs');
518
+ writeFileSync(output, buf);
519
+ return { content: [{ type: 'text', text: `Rendered slide ${slide} → ${output} (${buf.length} bytes)` }] };
520
+ }
521
+
522
+ // For inline display: convert to WebP (much smaller) via sharp
523
+ // Default to width=800 if no size specified to stay under MCP 1MB limit
524
+ const sharp = (await import('sharp')).default;
525
+ let img = sharp(buf);
526
+ if (!width && !scale) img = img.resize(800);
527
+ const webp = await img.webp({ quality: 80 }).toBuffer();
528
+
529
+ return {
530
+ content: [{
531
+ type: 'image',
532
+ data: webp.toString('base64'),
533
+ mimeType: 'image/webp',
534
+ }],
535
+ };
536
+ }
537
+ );
538
+
478
539
  // ── Start server ────────────────────────────────────────────────────────
479
540
  const transport = new StdioServerTransport();
480
541
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figmatk",
3
- "version": "0.3.1",
3
+ "version": "0.3.8",
4
4
  "description": "Figma Toolkit — Swiss-army knife CLI for Figma .deck and .fig files",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,7 @@
17
17
  "files": [
18
18
  "cli.mjs",
19
19
  "mcp-server.mjs",
20
+ "manifest.json",
20
21
  "lib/",
21
22
  "commands/",
22
23
  "skills/",
@@ -26,7 +27,12 @@
26
27
  "README.md"
27
28
  ],
28
29
  "scripts": {
29
- "start": "node cli.mjs"
30
+ "start": "node cli.mjs",
31
+ "pack": "node scripts/pack.mjs",
32
+ "release": "node scripts/release.mjs",
33
+ "validate:extension": "mcpb validate manifest.json",
34
+ "test": "vitest run",
35
+ "test:watch": "vitest"
30
36
  },
31
37
  "engines": {
32
38
  "node": ">=18"
@@ -42,13 +48,22 @@
42
48
  "author": "rcoenen",
43
49
  "dependencies": {
44
50
  "@modelcontextprotocol/sdk": "^1.27.1",
51
+ "@resvg/resvg-wasm": "^2.6.2",
45
52
  "archiver": "^7.0.1",
46
53
  "fzstd": "^0.1.1",
47
54
  "kiwi-schema": "^0.5.0",
48
55
  "pako": "^2.1.0",
49
56
  "sharp": "^0.34.5",
57
+ "ssim.js": "^3.5.0",
50
58
  "zstd-codec": "^0.1.5"
51
59
  },
60
+ "devDependencies": {
61
+ "@anthropic-ai/mcpb": "^2.1.2",
62
+ "@fontsource/darker-grotesque": "^5.2.8",
63
+ "@fontsource/inter": "^5.2.8",
64
+ "@fontsource/irish-grover": "^5.2.7",
65
+ "vitest": "^4.1.0"
66
+ },
52
67
  "keywords": [
53
68
  "figma",
54
69
  "deck",
@@ -1,29 +1,15 @@
1
1
  ---
2
2
  name: figma-slides-creator
3
3
  description: >
4
- Create, populate, edit, and inspect Figma Slides .deck files. Use when the
5
- user wants a finished presentation deck, wants to fill an existing template
6
- with content, or wants to edit a non-template deck's text, images, or slide
7
- order. Do not use this skill to author reusable templates themselves.
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
  Powered by FigmaTK under the hood.
9
8
  metadata:
10
- version: "0.3.1"
9
+ version: "0.3.8"
11
10
  ---
12
11
 
13
- # Figma Slides Creator
14
-
15
- Use this skill for the default workflow: take an existing template and build a new presentation from it. For authoring reusable templates themselves, use `skills/figma-template-builder/SKILL.md`.
16
-
17
- ## Skill Boundary
18
-
19
- Use this skill when the outcome is a finished deck for immediate use.
20
-
21
- Switch to `skills/figma-template-builder/SKILL.md` when the user wants to:
22
-
23
- - build a reusable template
24
- - define layouts or placeholders
25
- - rename slots for future sessions
26
- - derive a new template system from references or examples
12
+ # FigmaTK Skill
27
13
 
28
14
  ## ⚠️ Never open .deck files directly
29
15
 
@@ -37,144 +23,39 @@ To let the user view the result: tell them to **open the file in Figma Desktop**
37
23
 
38
24
  | Task | Approach |
39
25
  |------|----------|
40
- | Create from scratch | **Path A** `figmatk_create_deck` MCP tool |
41
- | Create from a `.deck` template | **Path B** `figmatk_list_template_layouts` + `figmatk_create_from_template` |
42
- | Author a reusable template | Use `skills/figma-template-builder/SKILL.md` |
43
- | Edit text or images in an existing deck | `figmatk_update_text`, `figmatk_insert_image` |
44
- | Clone, remove, or restructure slides | `figmatk_clone_slide`, `figmatk_remove_slide` |
45
- | Inspect structure or read content | `figmatk_inspect`, `figmatk_list_text` |
46
-
47
- ---
48
-
49
- ## File locations — always use /tmp
50
-
51
- **All files go in `/tmp/`** — scripts, output decks, images, everything. Never write to the Desktop, Documents, Downloads, or any user directory. Never create intermediate notes or reference markdown files. Just build and save the deck.
52
-
53
- ## Default Workflow
54
-
55
- 1. Inspect the template or deck.
56
- 2. Pick the minimum set of layouts or edits needed.
57
- 3. Populate text slots first, then image slots.
58
- 4. Save to a new `/tmp/` output path.
59
- 5. Sanity-check the result with `figmatk_list_text` or by opening it in Figma Desktop.
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`) |
60
30
 
61
31
  ---
62
32
 
63
- ## Path B — Create from a Template (preferred when user provides a .deck file)
64
-
65
- Use this path when the user provides a `.deck` template file. The output deck inherits all fonts, colors, spacing, and visual design from the template verbatim.
66
-
67
- ### Step 1 — Inspect the template
68
-
69
- ```
70
- figmatk_list_template_layouts("/path/to/template.deck")
71
- ```
72
-
73
- Returns a catalog of all available slide layouts. Each entry includes:
74
- - `slideId` — the ID to reference this layout
75
- - Layout state — `draft` or `published`
76
- - Text slots — explicit `slot:text:*` fields when present, otherwise fallback text candidates
77
- - Image slots — explicit `slot:image:*` fields when present, otherwise fallback image candidates
78
- - Node IDs — usable for direct targeting when the template has not been fully annotated yet
79
-
80
- **Read the catalog carefully before picking layouts:**
81
- - Prefer layouts with explicit slot metadata when available
82
- - Match each slide's purpose to your content; the existing copy is often the best hint
83
- - Use slot names first, then node IDs, then raw node names when populating content
84
- - If a layout exposes no explicit image slots, treat heuristic image candidates as weaker signals and avoid overwriting decorative sample imagery unless the user clearly wants that
85
-
86
- ### Step 2 — Create the deck
87
-
88
- ```
89
- figmatk_create_from_template({
90
- template: "/path/to/template.deck",
91
- output: "/tmp/my-deck.deck",
92
- slides: [
93
- { slideId: "1:74", text: { "title": "My Company" } },
94
- { slideId: "1:112", text: { "header": "The problem.", "body": "Description here." }, images: { "hero_image": "/tmp/problem-photo.jpg" } },
95
- { slideId: "1:643", text: { "title": "Thank you!" } }
96
- ]
97
- })
98
- ```
99
-
100
- Only pass slots or node IDs that exist in the layout's catalog. Extra keys are silently ignored.
101
-
102
- ---
103
-
104
- ## Path A — Create from Scratch (MCP tool — no template)
105
-
106
- **Always use this path.** No npm install, no scripts, no workspace setup.
107
-
108
- Call `figmatk_create_deck` with a structured slide description:
109
-
110
- ```json
111
- {
112
- "output": "/tmp/my-deck.deck",
113
- "title": "My Presentation",
114
- "theme": "midnight",
115
- "slides": [
116
- { "type": "title", "title": "My Presentation", "subtitle": "A subtitle" },
117
- { "type": "bullets", "title": "Key Points", "bullets": ["Point one", "Point two", "Point three"] },
118
- { "type": "two-column", "title": "Comparison", "leftText": "Left side content", "rightText": "Right side content" },
119
- { "type": "stat", "title": "By the numbers", "stat": "42%", "caption": "of users prefer this" },
120
- { "type": "image-full", "image": "/tmp/photo.jpg", "title": "Caption text" },
121
- { "type": "closing", "title": "Thank you", "subtitle": "Questions?" }
122
- ]
123
- }
124
- ```
125
-
126
- ### Slide types
127
-
128
- | Type | Fields |
129
- |------|--------|
130
- | `title` | `title`, `subtitle` |
131
- | `bullets` | `title`, `bullets` (array) |
132
- | `two-column` | `title`, `leftText`, `rightText`, `image` (right side) |
133
- | `stat` | `title`, `stat` (big number), `caption` |
134
- | `image-full` | `image` (path), `title`, `body` (overlay text) |
135
- | `closing` | `title`, `subtitle` |
136
-
137
- ### Themes
33
+ ## Path A — Create from Scratch (High-Level API)
138
34
 
139
- `midnight` · `ocean` · `forest` · `coral` · `terracotta` · `minimal`
35
+ Use this when the user wants a new presentation. Write a Node.js script and execute it.
140
36
 
141
- Each theme handles backgrounds, accent colors, and text colors automatically.
142
-
143
- ---
144
-
145
- ## Path A2 — Create from Scratch (Node.js script fallback)
146
-
147
- Only use this if `figmatk_create_deck` is unavailable or you need layout control beyond what the MCP tool offers.
148
-
149
- ### Step 1 — Set up workspace (MANDATORY — never skip)
150
-
151
- ```bash
152
- [ -d /tmp/figmatk-ws/node_modules ] || (mkdir -p /tmp/figmatk-ws && cd /tmp/figmatk-ws && npm init -y && npm install figmatk)
153
- ```
154
-
155
- ### Step 2 — Write script to `/tmp/figmatk-ws/deck.mjs`
156
-
157
- **Always use bare specifier** `import { Deck } from 'figmatk'` — never a file path.
37
+ > **Import path:** `figmatk` is an npm package. Import from the installed package:
38
+ > ```javascript
39
+ > import { Deck } from 'figmatk';
40
+ > ```
158
41
 
159
42
  ```javascript
160
43
  import { Deck } from 'figmatk';
161
44
 
162
- function hex(h) {
163
- 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 };
164
- }
165
-
166
45
  const deck = await Deck.create('My Presentation');
167
- const slide = deck.addBlankSlide();
168
- slide.setBackground('Black');
169
- slide.addText('Slide Title', { style: 'Title', color: 'White', x: 64, y: 80, width: 1792, align: 'LEFT' });
170
- await deck.save('/tmp/my-presentation.deck');
171
- console.log('Done');
172
- ```
173
46
 
174
- ### Step 3 Run
175
-
176
- ```bash
177
- node /tmp/figmatk-ws/deck.mjs
47
+ const slide = deck.addBlankSlide(); // template blank slide auto-removed
48
+ slide.setBackground('Black'); // named color — see list below
49
+ slide.addText('Slide Title', {
50
+ style: 'Title', color: 'White',
51
+ x: 64, y: 80, width: 1792, align: 'LEFT'
52
+ });
53
+ slide.addText('Subtitle', {
54
+ style: 'Body 1', color: 'Grey',
55
+ x: 64, y: 240, width: 1200, align: 'LEFT'
56
+ });
57
+
58
+ await deck.save('/path/to/output.deck');
178
59
  ```
179
60
 
180
61
  ### ⚠️ Critical gotchas
@@ -184,11 +65,9 @@ node /tmp/figmatk-ws/deck.mjs
184
65
  | `setBackground` with hex | `s.setBackground('#1A1A1A')` | `s.setBackground('Black')` |
185
66
  | `setBackground` with raw RGB | `s.setBackground({ r:0.1, g:0.1, b:0.1 })` | `s.setBackground('Black')` — raw RGB silently renders white |
186
67
  | Shape method signature | `s.addRectangle({ x:0, y:0, width:100 })` | `s.addRectangle(0, 0, 100, 100, opts)` |
187
- | Shape fill color | `{ fill: { r:1, g:0, b:0 } }` | `{ fill: '#F4900C' }` or `{ fill: 'Red' }` — hex strings and named colors work directly |
68
+ | Shape fill color | `{ fill: '#F4900C' }` | `{ fill: hex('#F4900C') }` — use the hex() helper |
188
69
  | `addLine` options | `{ strokeColor: ..., strokeWeight: 2 }` | `{ color: 'Black', weight: 2 }` |
189
70
  | `align` value | `align: 'left'` | `align: 'LEFT'` (uppercase) |
190
- | `addImage` without await | `slide.addImage('/tmp/photo.jpg')` | `await slide.addImage('/tmp/photo.jpg')` — async, images silently missing without await |
191
- | `addImage` old signature | `await slide.addImage(x, y, w, h, path)` | `await slide.addImage(path, { x, y, width, height })` — path is first arg now |
192
71
 
193
72
  ### Hex color helper (for shape fills)
194
73
 
@@ -212,17 +91,13 @@ function hex(h) {
212
91
  | `Body 3` | 24pt | Regular | Captions, labels |
213
92
  | `Note` | 20pt | Regular | Footnotes, sources |
214
93
 
215
- ### Colors for `setBackground()`
94
+ ### Named colors for `setBackground()`
216
95
 
217
- Accepts named colors, hex strings, or designer aliases — **when using figmatk 0.0.12+ from the workspace install**.
218
-
219
- **Named colors** (case-sensitive, from the Light Slides theme):
96
+ > **Case-sensitive.** `'Black'` works, `'black'` does not.
220
97
 
221
98
  `'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'`
222
99
 
223
- **Hex strings** (0.0.12+): `slide.setBackground('#C8102E')`
224
-
225
- **Designer aliases** (0.0.12+): `slide.setBackground('navy')`, `slide.setBackground('coral')`, `slide.setBackground('terracotta')` etc.
100
+ Use `'Black'` for dark backgrounds, `'White'` for light. For custom slide backgrounds, use the closest named color — **not hex**.
226
101
 
227
102
  ### Slide dimensions
228
103
 
@@ -240,8 +115,8 @@ slide.addDiamond(x, y, width, height, opts)
240
115
  slide.addTriangle(x, y, width, height, opts)
241
116
  slide.addStar(x, y, width, height, opts)
242
117
  slide.addLine(x1, y1, x2, y2, opts) // opts: color, weight
243
- await slide.addImage(pathOrBuf, opts) // ⚠️ ASYNC — must use await; opts: x, y, width, height (default: full slide 1920×1080), cornerRadius, opacity
244
- slide.addTable(x, y, data, opts) // 2D string array; opts: width, colWidths, rowHeight
118
+ slide.addImage(path, opts) // opts: x, y, width, height
119
+ slide.addTable(data, opts) // 2D string array; opts: x, y, width, colWidths, rowHeight
245
120
  slide.addSVG(x, y, width, svgPathOrBuf, opts)
246
121
  ```
247
122
 
@@ -266,7 +141,6 @@ Use this when the user provides a `.deck` file to modify.
266
141
 
267
142
  | Tool | Purpose |
268
143
  |------|---------|
269
- | `figmatk_create_deck` | **Create a new deck from scratch** — no npm install needed |
270
144
  | `figmatk_inspect` | Node hierarchy tree — structure, node IDs, slide count |
271
145
  | `figmatk_list_text` | All text strings and image hashes per slide |
272
146
  | `figmatk_list_overrides` | Editable override keys per symbol (component) |
@@ -275,45 +149,55 @@ Use this when the user provides a `.deck` file to modify.
275
149
  | `figmatk_clone_slide` | Deep-clone a slide with new text and images |
276
150
  | `figmatk_remove_slide` | Mark slides as REMOVED (never deleted) |
277
151
  | `figmatk_roundtrip` | Decode + re-encode for pipeline validation |
278
-
279
- ## Final Checks
280
-
281
- Before finishing, prefer at least one of these:
282
-
283
- - `figmatk_list_text` on the output deck
284
- - `figmatk_roundtrip` if the deck went through multiple edits
285
- - a manual open check in Figma Desktop when the user is validating upload/render behavior
152
+ | `figmatk_render_slide` | Render a slide to image (inline WebP or saved PNG) |
286
153
 
287
154
  ---
288
155
 
289
- ## Design Philosophy
156
+ ## Path C — Visual QA (Render + Inspect)
290
157
 
291
- Think like a **professional PowerPoint designer**, not an AI generating slides. Every deck must feel like it was made by a human who spent a day on it.
158
+ After creating or modifying a deck, **always render and visually inspect** the output. This catches issues that text inspection misses: overflowing text, broken layouts, wrong colors, misaligned elements.
292
159
 
293
- ### Deck structure — use this template every time
294
-
295
- A proper deck has a clear spine. Follow this slide order:
160
+ ### Workflow
296
161
 
297
- | # | Slide type | Purpose |
298
- |---|-----------|---------|
299
- | 1 | **Title** | Dark bg, big title, subtitle, presenter name |
300
- | 2 | **Agenda / Overview** | 3–5 bullet topics, light bg |
301
- | 3–N | **Content slides** | Vary layout each slide — see below |
302
- | N+1 | **Section divider** (optional) | Bold colour block to signal a new chapter |
303
- | Last | **Closing / CTA** | Dark bg mirrors title slide — "Thank you", next steps, contact |
162
+ 1. Render each slide at preview size (returns inline WebP image):
163
+ ```
164
+ figmatk_render_slide(path: "/tmp/my-deck.deck", slide: 1)
165
+ ```
166
+ 2. Inspect the returned image for:
167
+ - Text overflowing its bounding box or clipped
168
+ - Layout misalignment or overlapping elements
169
+ - Wrong colors or missing backgrounds
170
+ - Missing images or broken fills
171
+ 3. If issues are found, fix them and re-render
172
+ 4. For full-resolution export:
173
+ ```
174
+ figmatk_render_slide(path: "/tmp/my-deck.deck", slide: 1, output: "/tmp/slide-1.png")
175
+ ```
176
+
177
+ ### Render options
178
+
179
+ | Option | Example | Effect |
180
+ |--------|---------|--------|
181
+ | (none) | `slide: 1` | Inline WebP at 800px wide (for QA) |
182
+ | `width` | `width: 400` | Resize to 400px wide (proportional) |
183
+ | `scale` | `scale: "50%"` | Half size (960×540) |
184
+ | `output` | `output: "/tmp/s.png"` | Save full PNG to disk |
185
+
186
+ ### CLI alternative
304
187
 
305
- The title and closing slides must use the **same dark background** — this creates the "sandwich" effect that makes decks feel complete.
188
+ ```bash
189
+ figmatk render my-deck.deck -o /tmp/renders/ # all slides
190
+ figmatk render my-deck.deck -o /tmp/renders/ --slide 3 # single slide
191
+ figmatk render my-deck.deck -o /tmp/renders/ --width 400 # thumbnail size
192
+ ```
306
193
 
307
- ### Consistent visual motif pick one and use it on every slide
194
+ **Important:** Always run visual QA on every deck you create or modify. Do not skip this step.
308
195
 
309
- Choose one repeating element and place it consistently across all content slides:
196
+ ---
310
197
 
311
- - **Top accent bar**: `addRectangle(0, 0, 1920, 8, { fill: hex('#...') })` — full-width coloured strip at top
312
- - **Left colour panel**: tall rectangle on the left third, text floats right
313
- - **Corner badge**: small filled circle or square in bottom-right with slide number or logo
314
- - **Bottom rule**: thin full-width line at y=1040
198
+ ## Design Philosophy
315
199
 
316
- Without a motif, slides look unrelated. With one, the deck feels designed.
200
+ Every deck must look **intentionally designed**, not AI-generated.
317
201
 
318
202
  ### Colour
319
203
 
@@ -332,29 +216,18 @@ Without a motif, slides look unrelated. With one, the deck feels designed.
332
216
  | Ocean | `'Blue'` | `hex('#21295C')` | `'White'` |
333
217
  | Minimal | `'White'` | `hex('#36454F')` | `'Black'` |
334
218
 
335
- ### Layout — vary every slide
336
-
337
- Each content slide should use a **different layout type**. Never repeat the same structure back-to-back.
219
+ ### Layout
338
220
 
339
- | Layout | When to use |
340
- |--------|------------|
341
- | Two-column | Comparison, pros/cons, text + image |
342
- | 2×2 or 2×3 grid | Features, icons, categories |
343
- | Large stat callout | One big number + explanation |
344
- | Half-background image | Photo-rich slides |
345
- | Timeline / steps | Process, history, roadmap |
346
- | Icon + text rows | Lists that need visual weight |
347
- | Full-bleed image | Impact moment, section break |
221
+ - Every slide needs at least **one visual element** — shape, image, SVG, or table.
222
+ - **Vary layouts** — never repeat the same structure slide after slide.
223
+ - Carry one visual motif through every slide (coloured accent bar, icon circles, etc.).
348
224
 
349
- Every slide needs at least **one visual element** shape, image, SVG, or table. No text-only slides.
225
+ **Layout options:** two-column, icon+text rows, 2×2/2×3 grid, large stat callout, half-background image, timeline/steps.
350
226
 
351
227
  ### Typography
352
228
 
353
- - Left-align body text. Centre only titles on title/closing slides.
229
+ - Left-align body text. Centre only titles.
354
230
  - Minimum 64px margin from slide edges. 24–48px between content blocks.
355
- - Use `Header 2` or `Header 3` for slide titles on content slides (not `Title` — that's for the title slide only).
356
- - **Body text: max 2 sentences per text block.** Text boxes have fixed heights — overflow gets clipped. If you have more to say, use a bullet list or split across slides.
357
- - **Bullets: max 6 items, max 8 words per bullet.** Longer bullets wrap and push content off-slide.
358
231
 
359
232
  ### Never do
360
233
 
@@ -362,17 +235,17 @@ Every slide needs at least **one visual element** — shape, image, SVG, or tabl
362
235
  - Centre body text
363
236
  - Use accent lines under slide titles (hallmark of AI-generated slides)
364
237
  - Text-only slides
365
- - Low-contrast text against background — **match image tone to slide palette**: dark/moody images on light-background slides make text unreadable; pick a bright image or switch to a dark-background layout
366
- - Skip the closing slide — it makes the deck feel unfinished
367
- - Put long paragraphs in body/caption fields — text overflows the container
238
+ - Low-contrast text against background
368
239
 
369
240
  ---
370
241
 
371
242
  ## QA
372
243
 
373
244
  1. Self-check: no placeholder text (`lorem ipsum`, `[title here]`) remains
374
- 2. Tell the user to open the `.deck` in Figma Desktop to catch rendering issues
375
- 3. Offer to fix anything they report
245
+ 2. **Render every slide** using `figmatk_render_slide` and visually inspect for overflows, clipping, alignment, and color issues
246
+ 3. Fix any issues found and re-render to confirm
247
+ 4. Tell the user to open the `.deck` in Figma Desktop for final review
248
+ 5. Offer to fix anything they report
376
249
 
377
250
  ---
378
251
 
@@ -6,13 +6,23 @@ description: >
6
6
  template from an existing deck, define reusable layouts, mark editable
7
7
  text/image slots, or prepare a draft template for later instantiation.
8
8
  metadata:
9
- version: "0.1.0"
9
+ version: "0.3.8"
10
10
  ---
11
11
 
12
12
  # Figma Template Builder
13
13
 
14
14
  Use this skill when the goal is to build or refine the template itself. For the common workflow of taking an existing template and producing a new presentation, use `skills/figma-slides-creator/SKILL.md`.
15
15
 
16
+ ## MCP First
17
+
18
+ In Claude Cowork, keep template authoring inside the MCP plugin.
19
+
20
+ - Do not inspect the installed `figmatk` package to discover template features.
21
+ - Do not write direct Node.js scripts for draft creation, annotation, wrapping, or instantiation when MCP tools exist for those steps.
22
+ - Prefer `figmatk_create_template_draft`, `figmatk_annotate_template_layout`, `figmatk_publish_template_draft`, and `figmatk_list_template_layouts`.
23
+
24
+ Only fall back to direct library code if the MCP server is unavailable or the required capability is missing from the MCP surface.
25
+
16
26
  ## Skill Boundary
17
27
 
18
28
  Use this skill when the deliverable is a reusable template, not a one-off deck.