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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +18 -1
- package/lib/fig-deck.mjs +4 -4
- package/lib/template-deck.mjs +573 -148
- package/mcp-server.mjs +121 -18
- package/package.json +1 -1
- package/skills/figma-slides-creator/SKILL.md +47 -13
- package/skills/figma-template-builder/SKILL.md +148 -0
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 {
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
|
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' ||
|
|
100
|
-
lines.push(` ${' '.repeat(depth)}${type} ${
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
|
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
|
|
351
|
-
|
|
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
|
|
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
|
|
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,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
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
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
|
-
-
|
|
53
|
-
-
|
|
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
|
|
54
79
|
|
|
55
80
|
**Read the catalog carefully before picking layouts:**
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
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: { "
|
|
68
|
-
{ slideId: "1:112", text: { "
|
|
69
|
-
{ slideId: "1:643", text: { "
|
|
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
|
|
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
|
+
```
|