figmatk 0.3.0 → 0.3.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.
package/mcp-server.mjs CHANGED
@@ -7,9 +7,15 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
7
7
  import { z } from 'zod';
8
8
  import { FigDeck } from './lib/fig-deck.mjs';
9
9
  import { Deck } from './lib/api.mjs';
10
- import { listTemplateLayouts, createFromTemplate } from './lib/template-deck.mjs';
11
- import { nid, ov, nestedOv, removeNode, parseId, positionChar } from './lib/node-helpers.mjs';
12
- import { imageOv, hexToHash, hashToHex } from './lib/image-helpers.mjs';
10
+ import {
11
+ annotateTemplateLayout,
12
+ createDraftTemplate,
13
+ createFromTemplate,
14
+ listTemplateLayouts,
15
+ publishTemplateDraft,
16
+ } from './lib/template-deck.mjs';
17
+ import { nid, ov, removeNode } from './lib/node-helpers.mjs';
18
+ import { imageOv, hashToHex } from './lib/image-helpers.mjs';
13
19
  import { deepClone } from './lib/deep-clone.mjs';
14
20
 
15
21
  const server = new McpServer({
@@ -46,7 +52,7 @@ server.tool(
46
52
  // ── list-text ───────────────────────────────────────────────────────────
47
53
  server.tool(
48
54
  'figmatk_list_text',
49
- 'List all text and image content per slide in a .deck file',
55
+ 'List visible text and image content per slide in a .deck file, including direct slide nodes and instance overrides.',
50
56
  { path: z.string().describe('Path to .deck or .fig file') },
51
57
  async ({ path }) => {
52
58
  const deck = await FigDeck.fromDeckFile(path);
@@ -56,18 +62,41 @@ server.tool(
56
62
  if (slide.phase === 'REMOVED') continue;
57
63
  const id = nid(slide);
58
64
  lines.push(`\n── Slide ${id} "${slide.name || ''}" ──`);
65
+
66
+ const directLines = [];
67
+ deck.walkTree(id, (node, depth) => {
68
+ if (depth === 0 || node.phase === 'REMOVED') return;
69
+ if (node.type === 'TEXT' && node.textData?.characters) {
70
+ directLines.push(` [text-node] ${nid(node)} "${node.name || ''}": ${node.textData.characters.substring(0, 120)}`);
71
+ }
72
+ if (node.type === 'SHAPE_WITH_TEXT' && node.nodeGenerationData?.overrides) {
73
+ for (const override of node.nodeGenerationData.overrides) {
74
+ if (override.textData?.characters) {
75
+ directLines.push(` [shape-text] ${nid(node)} "${node.name || ''}": ${override.textData.characters.substring(0, 120)}`);
76
+ break;
77
+ }
78
+ }
79
+ }
80
+ const imageFill = node.fillPaints?.find(p => p.type === 'IMAGE' && p.image?.hash);
81
+ if (imageFill) {
82
+ directLines.push(` [image-node] ${nid(node)} "${node.name || ''}": ${hashToHex(imageFill.image.hash)}`);
83
+ }
84
+ });
85
+
86
+ lines.push(...directLines);
87
+
59
88
  const inst = deck.getSlideInstance(id);
60
89
  if (!inst?.symbolData?.symbolOverrides) continue;
61
90
  for (const ov of inst.symbolData.symbolOverrides) {
62
91
  const key = ov.guidPath?.guids?.[0];
63
92
  const keyStr = key ? `${key.sessionID}:${key.localID}` : '?';
64
93
  if (ov.textData?.characters) {
65
- lines.push(` [text] ${keyStr}: ${ov.textData.characters.substring(0, 120)}`);
94
+ lines.push(` [text-override] ${keyStr}: ${ov.textData.characters.substring(0, 120)}`);
66
95
  }
67
96
  if (ov.fillPaints?.length) {
68
97
  for (const p of ov.fillPaints) {
69
98
  if (p.image?.hash) {
70
- lines.push(` [image] ${keyStr}: ${hashToHex(p.image.hash)}`);
99
+ lines.push(` [image-override] ${keyStr}: ${hashToHex(p.image.hash)}`);
71
100
  }
72
101
  }
73
102
  }
@@ -93,13 +122,13 @@ server.tool(
93
122
  function walkChildren(nodeId, depth) {
94
123
  const node = deck.getNode(nodeId);
95
124
  if (!node || node.phase === 'REMOVED') return;
96
- const cid = nid(node);
125
+ const key = node.overrideKey ? `${node.overrideKey.sessionID}:${node.overrideKey.localID}` : null;
97
126
  const type = node.type || '?';
98
127
  const name = node.name || '';
99
- if (type === 'TEXT' || (node.fillPaints?.some(p => p.type === 'IMAGE'))) {
100
- lines.push(` ${' '.repeat(depth)}${type} ${cid} "${name}"`);
128
+ if (key && (type === 'TEXT' || node.fillPaints?.some(p => p.type === 'IMAGE'))) {
129
+ lines.push(` ${' '.repeat(depth)}${type} ${key} "${name}"`);
101
130
  }
102
- const kids = deck.childrenMap.get(cid) || [];
131
+ const kids = deck.childrenMap.get(nid(node)) || [];
103
132
  for (const kid of kids) walkChildren(nid(kid), depth + 1);
104
133
  }
105
134
  for (const child of children) walkChildren(nid(child), 0);
@@ -127,7 +156,18 @@ server.tool(
127
156
 
128
157
  for (const [key, text] of Object.entries(overrides)) {
129
158
  const [s, l] = key.split(':').map(Number);
130
- inst.symbolData.symbolOverrides.push(ov({ sessionID: s, localID: l }, text));
159
+ const nextOverride = ov({ sessionID: s, localID: l }, text);
160
+ const existingIdx = inst.symbolData.symbolOverrides.findIndex(entry =>
161
+ entry.guidPath?.guids?.length >= 1 &&
162
+ entry.guidPath.guids[0].sessionID === s &&
163
+ entry.guidPath.guids[0].localID === l &&
164
+ entry.textData
165
+ );
166
+ if (existingIdx >= 0) {
167
+ inst.symbolData.symbolOverrides.splice(existingIdx, 1, nextOverride);
168
+ } else {
169
+ inst.symbolData.symbolOverrides.push(nextOverride);
170
+ }
131
171
  }
132
172
 
133
173
  const bytes = await deck.saveDeck(output);
@@ -158,9 +198,18 @@ server.tool(
158
198
  if (!inst.symbolData.symbolOverrides) inst.symbolData.symbolOverrides = [];
159
199
 
160
200
  const [s, l] = targetKey.split(':').map(Number);
161
- inst.symbolData.symbolOverrides.push(
162
- imageOv({ sessionID: s, localID: l }, imageHash, thumbHash, width, height)
201
+ const nextOverride = imageOv({ sessionID: s, localID: l }, imageHash, thumbHash, width, height);
202
+ const existingIdx = inst.symbolData.symbolOverrides.findIndex(entry =>
203
+ entry.guidPath?.guids?.length >= 1 &&
204
+ entry.guidPath.guids[0].sessionID === s &&
205
+ entry.guidPath.guids[0].localID === l &&
206
+ entry.fillPaints
163
207
  );
208
+ if (existingIdx >= 0) {
209
+ inst.symbolData.symbolOverrides.splice(existingIdx, 1, nextOverride);
210
+ } else {
211
+ inst.symbolData.symbolOverrides.push(nextOverride);
212
+ }
164
213
 
165
214
  const opts = imagesDir ? { imagesDir } : {};
166
215
  const bytes = await deck.saveDeck(output, opts);
@@ -338,17 +387,70 @@ server.tool(
338
387
  );
339
388
 
340
389
  // ── figmatk_list_template_layouts ────────────────────────────────────────
390
+ server.tool(
391
+ 'figmatk_create_template_draft',
392
+ 'Create a new draft template deck. Draft templates are normal slide decks; later annotate slots and publish-wrap them into module-backed layouts.',
393
+ {
394
+ output: z.string().describe('Output path for the draft template .deck file'),
395
+ title: z.string().describe('Template deck title'),
396
+ layouts: z.array(z.string()).optional().describe('Optional ordered list of layout names to create, e.g. ["cover", "agenda", "section"]'),
397
+ },
398
+ async ({ output, title, layouts }) => {
399
+ const bytes = await createDraftTemplate(output, { title, layouts });
400
+ return { content: [{ type: 'text', text: `Created draft template ${output} (${bytes} bytes). Use figmatk_annotate_template_layout to mark layout and slot names.` }] };
401
+ }
402
+ );
403
+
404
+ server.tool(
405
+ 'figmatk_annotate_template_layout',
406
+ 'Add explicit layout and slot metadata to a draft or published template. Use figmatk_inspect or figmatk_list_template_layouts first to get slide and node IDs.',
407
+ {
408
+ path: z.string().describe('Path to the source .deck file'),
409
+ output: z.string().describe('Output path for the updated .deck file'),
410
+ slideId: z.string().describe('Slide node ID to annotate'),
411
+ layoutName: z.string().optional().describe('Logical layout name without the layout: prefix, e.g. "cover"'),
412
+ textSlots: z.record(z.string()).optional().describe('Map of nodeId -> text slot name, e.g. {"1:120": "title"}'),
413
+ imageSlots: z.record(z.string()).optional().describe('Map of nodeId -> image slot name, e.g. {"1:144": "hero_image"}'),
414
+ fixedImages: z.record(z.string()).optional().describe('Map of nodeId -> fixed image label for decorative/sample content'),
415
+ },
416
+ async ({ path, output, slideId, layoutName, textSlots, imageSlots, fixedImages }) => {
417
+ const bytes = await annotateTemplateLayout(path, output, { slideId, layoutName, textSlots, imageSlots, fixedImages });
418
+ return { content: [{ type: 'text', text: `Annotated slide ${slideId}. Saved ${output} (${bytes} bytes).` }] };
419
+ }
420
+ );
421
+
422
+ server.tool(
423
+ 'figmatk_publish_template_draft',
424
+ 'Wrap draft template slides in publish-like MODULE nodes while preserving the slide subtree and internal canvas assets.',
425
+ {
426
+ path: z.string().describe('Path to the draft template .deck file'),
427
+ output: z.string().describe('Output path for the wrapped .deck file'),
428
+ slideIds: z.array(z.string()).optional().describe('Optional list of draft slide IDs to wrap. Defaults to every draft layout on the main canvas.'),
429
+ },
430
+ async ({ path, output, slideIds }) => {
431
+ const bytes = await publishTemplateDraft(path, output, { slideIds });
432
+ return { content: [{ type: 'text', text: `Publish-wrapped draft template to ${output} (${bytes} bytes).` }] };
433
+ }
434
+ );
435
+
341
436
  server.tool(
342
437
  'figmatk_list_template_layouts',
343
- 'Inspect a Figma .deck template and return available slide layouts with their text field names. Call this first before figmatk_create_from_template.',
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.',
344
439
  {
345
440
  template: z.string().describe('Path to the .deck template file'),
346
441
  },
347
442
  async ({ template }) => {
348
443
  const layouts = await listTemplateLayouts(template);
349
444
  const lines = layouts.map(l => {
350
- const fields = l.textFields.map(f => ` - "${f.name}" (${f.nodeId}): "${f.preview}"`).join('\n');
351
- return `Slide "${l.name}" [${l.slideId}]\n${fields || ' (no text fields)'}`;
445
+ const textSlots = l.textFields.map(f => ` - ${f.name} (${f.nodeId}, ${f.source}): "${f.preview}"`).join('\n');
446
+ const imageSlots = l.imagePlaceholders.map(f => ` - ${f.name} (${f.nodeId}, ${f.source}, ${f.width}x${f.height})${f.hasCurrentImage ? ' [image]' : ''}`).join('\n');
447
+ return [
448
+ `Layout "${l.name}" [${l.slideId}]`,
449
+ ` state: ${l.state}${l.moduleId ? `, module ${l.moduleId}` : ''}, row ${l.rowId}`,
450
+ ` explicit slots: ${l.hasExplicitSlotMetadata ? 'yes' : 'no'}`,
451
+ textSlots ? ` text slots:\n${textSlots}` : ' text slots: (none)',
452
+ imageSlots ? ` image slots:\n${imageSlots}` : ' image slots: (none)',
453
+ ].join('\n');
352
454
  });
353
455
  return { content: [{ type: 'text', text: lines.join('\n\n') }] };
354
456
  }
@@ -357,13 +459,14 @@ server.tool(
357
459
  // ── figmatk_create_from_template ─────────────────────────────────────────
358
460
  server.tool(
359
461
  'figmatk_create_from_template',
360
- 'Create a new Figma Slides deck by cherry-picking layouts from a template .deck file and populating them with content. Preserves all template colors, fonts, and styling.',
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.',
361
463
  {
362
464
  template: z.string().describe('Path to the source .deck template file'),
363
465
  output: z.string().describe('Output path for the new .deck file (use /tmp/)'),
364
466
  slides: z.array(z.object({
365
467
  slideId: z.string().describe('Slide ID from figmatk_list_template_layouts (e.g. "1:74")'),
366
- text: z.record(z.string()).optional().describe('Map of text field name value (e.g. { "Title": "My Company", "Body 1": "..." })'),
468
+ text: z.record(z.string()).optional().describe('Map of text slot/name/nodeId -> value (e.g. { "title": "My Company" })'),
469
+ images: z.record(z.string()).optional().describe('Map of image slot/name/nodeId -> absolute image path (e.g. { "hero_image": "/tmp/photo.jpg" })'),
367
470
  })).describe('Ordered list of slides to include, each referencing a template layout'),
368
471
  },
369
472
  async ({ template, output, slides }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figmatk",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Figma Toolkit — Swiss-army knife CLI for Figma .deck and .fig files",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,16 +1,30 @@
1
1
  ---
2
2
  name: figma-slides-creator
3
3
  description: >
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.
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.
7
8
  Powered by FigmaTK under the hood.
8
9
  metadata:
9
- version: "0.3.0"
10
+ version: "0.3.1"
10
11
  ---
11
12
 
12
13
  # Figma Slides Creator
13
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
27
+
14
28
  ## ⚠️ Never open .deck files directly
15
29
 
16
30
  `.deck` files are binary ZIP archives. **Never open, read, or display a `.deck` file** — it will show garbage bytes in the panel. To inspect or modify a `.deck` file, always use the CLI commands or Node.js API shown below.
@@ -25,6 +39,7 @@ To let the user view the result: tell them to **open the file in Figma Desktop**
25
39
  |------|----------|
26
40
  | Create from scratch | **Path A** — `figmatk_create_deck` MCP tool |
27
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` |
28
43
  | Edit text or images in an existing deck | `figmatk_update_text`, `figmatk_insert_image` |
29
44
  | Clone, remove, or restructure slides | `figmatk_clone_slide`, `figmatk_remove_slide` |
30
45
  | Inspect structure or read content | `figmatk_inspect`, `figmatk_list_text` |
@@ -35,6 +50,14 @@ To let the user view the result: tell them to **open the file in Figma Desktop**
35
50
 
36
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.
37
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.
60
+
38
61
  ---
39
62
 
40
63
  ## Path B — Create from a Template (preferred when user provides a .deck file)
@@ -49,13 +72,16 @@ figmatk_list_template_layouts("/path/to/template.deck")
49
72
 
50
73
  Returns a catalog of all available slide layouts. Each entry includes:
51
74
  - `slideId` — the ID to reference this layout
52
- - Text fieldseditable TEXT nodes with their names and current content
53
- - Image placeholdersFRAME nodes with IMAGE fill (these need a real image)
75
+ - Layout state`draft` or `published`
76
+ - Text slotsexplicit `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
54
79
 
55
80
  **Read the catalog carefully before picking layouts:**
56
- - Match each slide's purpose to your content (the existing text in the template is a strong hint — e.g. "Use this slide to introduce the big problem" → use for your problem statement)
57
- - Slides with image placeholders need an appropriate image — the surrounding text should describe what's shown in that image
58
- - Slides with `SHAPE_WITH_TEXT` pill labels (MONTH XX YEAR, TAGLINE, CONFIDENTIAL) cannot be changed programmatically — tell the user to update those in Figma
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
59
85
 
60
86
  ### Step 2 — Create the deck
61
87
 
@@ -64,14 +90,14 @@ figmatk_create_from_template({
64
90
  template: "/path/to/template.deck",
65
91
  output: "/tmp/my-deck.deck",
66
92
  slides: [
67
- { slideId: "1:74", text: { "Title": "My Company" } },
68
- { slideId: "1:112", text: { "Header 1": "The problem.", "Body 1": "Description here." } },
69
- { slideId: "1:643", text: { "Thank you": "Thank you!" } }
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!" } }
70
96
  ]
71
97
  })
72
98
  ```
73
99
 
74
- Only pass text fields that exist in the layout's catalog extra fields are silently ignored.
100
+ Only pass slots or node IDs that exist in the layout's catalog. Extra keys are silently ignored.
75
101
 
76
102
  ---
77
103
 
@@ -250,6 +276,14 @@ Use this when the user provides a `.deck` file to modify.
250
276
  | `figmatk_remove_slide` | Mark slides as REMOVED (never deleted) |
251
277
  | `figmatk_roundtrip` | Decode + re-encode for pipeline validation |
252
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
286
+
253
287
  ---
254
288
 
255
289
  ## Design Philosophy
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: figma-template-builder
3
+ description: >
4
+ Author reusable Figma Slides templates as .deck files. Use when the user
5
+ wants to build a template from reference images or examples, derive a new
6
+ template from an existing deck, define reusable layouts, mark editable
7
+ text/image slots, or prepare a draft template for later instantiation.
8
+ metadata:
9
+ version: "0.1.0"
10
+ ---
11
+
12
+ # Figma Template Builder
13
+
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
+
16
+ ## Skill Boundary
17
+
18
+ Use this skill when the deliverable is a reusable template, not a one-off deck.
19
+
20
+ Stay here when the user wants to:
21
+
22
+ - translate reference images or screenshots into template layouts
23
+ - create a new layout family
24
+ - define placeholder semantics for later sessions
25
+ - turn an ordinary deck into a reusable template
26
+
27
+ Hand off to `skills/figma-slides-creator/SKILL.md` once the template exists and the task becomes simple content population.
28
+
29
+ ## Core Model
30
+
31
+ Template work happens in two states:
32
+
33
+ - Draft template: `SLIDE_ROW -> SLIDE -> ...`
34
+ - Published or publish-like template: `SLIDE_ROW -> MODULE -> SLIDE -> ...`
35
+
36
+ Draft templates are easier to author. Publish-like wrapping is the final step before later instantiation.
37
+
38
+ ## Design-First Workflow
39
+
40
+ When the user provides reference images, screenshots, or example decks:
41
+
42
+ 1. Read the references first and infer the layout family before touching the `.deck`.
43
+ 2. Decide which parts are reusable structure versus one-off sample content.
44
+ 3. Create only the smallest set of layouts that captures the system.
45
+ 4. Use semantic slot names so later sessions can populate them without re-reading the design intent.
46
+
47
+ Do not mirror every visual variation as its own layout unless the content structure changes materially.
48
+
49
+ ## Naming Conventions
50
+
51
+ Always use explicit metadata when authoring reusable layouts:
52
+
53
+ - Layouts: `layout:<name>`
54
+ - Text slots: `slot:text:<name>`
55
+ - Image slots: `slot:image:<name>`
56
+ - Decorative fixed imagery: `fixed:image:<name>`
57
+
58
+ These conventions are how later sessions discover what is editable.
59
+
60
+ Prefer semantic names over visual or auto-generated names.
61
+
62
+ Good:
63
+
64
+ - `title`
65
+ - `subtitle`
66
+ - `hero_image`
67
+ - `device_screen_primary`
68
+ - `quote_author`
69
+
70
+ Bad:
71
+
72
+ - `text1`
73
+ - `frame183`
74
+ - `image-left`
75
+ - `rectangle2`
76
+
77
+ ## Recommended Flow
78
+
79
+ ### 1. Create or inspect a draft template
80
+
81
+ - New draft from scratch: `figmatk_create_template_draft`
82
+ - Existing deck/template: `figmatk_list_template_layouts`
83
+ - Structural inspection: `figmatk_inspect`
84
+
85
+ ### 2. Annotate reusable layouts
86
+
87
+ Use `figmatk_annotate_template_layout` to:
88
+
89
+ - rename a slide as a layout
90
+ - mark text nodes as editable text slots
91
+ - mark image-bearing nodes as editable image slots
92
+ - mark decorative imagery as fixed
93
+
94
+ The tool accepts node ID maps, so inspect first if you need the raw node IDs.
95
+
96
+ While annotating:
97
+
98
+ - rename the slide itself to the stable layout name
99
+ - mark only true placeholders as `slot:*`
100
+ - mark decorative or sample imagery as `fixed:image:*`
101
+ - prefer stable semantic names over spatial names like `left_box`
102
+
103
+ ### 3. Publish-wrap when the template is ready
104
+
105
+ Use `figmatk_publish_template_draft` to add publish-like `MODULE` wrappers while preserving the slide subtree and internal assets.
106
+
107
+ ### 4. Verify the result
108
+
109
+ - `figmatk_list_template_layouts`
110
+ - `figmatk_list_text`
111
+ - `figmatk_roundtrip` if you want a conservative encode/decode check
112
+ - open the wrapped template in Figma Desktop when validating real upload behavior
113
+
114
+ ## Practical Rules
115
+
116
+ - Prefer explicit slot names over heuristic placeholders.
117
+ - Do not assume every image fill is editable content.
118
+ - Preserve `Internal Only Canvas` assets.
119
+ - Preserve special nodes such as device mockups and interactive slide elements; do not try to recreate them from scratch unless necessary.
120
+
121
+ ## Template Authoring Heuristics
122
+
123
+ - Start with 4-8 reusable layouts, not an exhaustive library.
124
+ - Reuse one layout when only copy length changes; create a new layout when hierarchy or media structure changes.
125
+ - Separate content slots from chrome. For device mockups, the screen is usually the slot and the hardware frame is usually fixed.
126
+ - If a layout has explicit slot metadata, do not rely on heuristic image placeholders for that layout.
127
+ - After publish-wrapping, re-run `figmatk_list_template_layouts` and confirm the layout names and slot names survived unchanged.
128
+
129
+ ## Example
130
+
131
+ ```json
132
+ {
133
+ "path": "/tmp/draft-template.deck",
134
+ "output": "/tmp/draft-template-annotated.deck",
135
+ "slideId": "1:42",
136
+ "layoutName": "cover",
137
+ "textSlots": {
138
+ "1:120": "title",
139
+ "1:121": "subtitle"
140
+ },
141
+ "imageSlots": {
142
+ "1:144": "hero_image"
143
+ },
144
+ "fixedImages": {
145
+ "1:199": "background_texture"
146
+ }
147
+ }
148
+ ```