@atomixstudio/mcp 1.0.34 → 1.0.35
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/README.md +10 -2
- package/dist/chunk-426RNS3G.js +1782 -0
- package/dist/chunk-426RNS3G.js.map +1 -0
- package/dist/figma-bridge-protocol.d.ts +4 -3
- package/dist/figma-bridge-protocol.js +1 -1
- package/dist/index.js +534 -742
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-CE6J5MJX.js +0 -49
- package/dist/chunk-CE6J5MJX.js.map +0 -1
|
@@ -0,0 +1,1782 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../figma-tools/dist/index.js
|
|
4
|
+
var LEGACY_GROUP_PREFIX = {
|
|
5
|
+
background: "bg",
|
|
6
|
+
text: "text",
|
|
7
|
+
icon: "icon",
|
|
8
|
+
border: "border",
|
|
9
|
+
brand: "brand",
|
|
10
|
+
action: "action",
|
|
11
|
+
feedback: "feedback"
|
|
12
|
+
};
|
|
13
|
+
var RESERVED_COLOR_KEYS = /* @__PURE__ */ new Set(["customScales", "neutral"]);
|
|
14
|
+
function getColorGroupOrder(storedColors) {
|
|
15
|
+
if (Array.isArray(storedColors._groupOrder) && storedColors._groupOrder.length > 0) {
|
|
16
|
+
return storedColors._groupOrder;
|
|
17
|
+
}
|
|
18
|
+
return Object.keys(storedColors).filter(
|
|
19
|
+
(k) => !k.startsWith("_") && !RESERVED_COLOR_KEYS.has(k) && typeof storedColors[k] === "object"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
function getGroupPrefix(storedColors, groupName) {
|
|
23
|
+
const custom = storedColors._groupPrefix;
|
|
24
|
+
if (custom && typeof custom[groupName] === "string") return custom[groupName];
|
|
25
|
+
return LEGACY_GROUP_PREFIX[groupName] ?? groupName;
|
|
26
|
+
}
|
|
27
|
+
function refScaleToFigmaDisplayName(scaleFromRef) {
|
|
28
|
+
const s = scaleFromRef.trim();
|
|
29
|
+
if (!s) return s;
|
|
30
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
31
|
+
}
|
|
32
|
+
function referenceToFigmaPrimitiveName(ref, primitiveNames) {
|
|
33
|
+
if (!ref || typeof ref !== "string") return null;
|
|
34
|
+
const r = ref.trim();
|
|
35
|
+
const scaleStep = /^([a-zA-Z]+)\.(\d+|[a-z]+)$/.exec(r);
|
|
36
|
+
if (scaleStep) {
|
|
37
|
+
const [, scale, step] = scaleStep;
|
|
38
|
+
const scaleDisplay = refScaleToFigmaDisplayName(scale);
|
|
39
|
+
const name = `${scaleDisplay} / ${step}`;
|
|
40
|
+
return primitiveNames.has(name) ? name : null;
|
|
41
|
+
}
|
|
42
|
+
const radixMatch = /^radix\.([a-zA-Z]+)\.(\d+)$/.exec(r);
|
|
43
|
+
if (radixMatch) {
|
|
44
|
+
const [, family, step] = radixMatch;
|
|
45
|
+
const scaleName = refScaleToFigmaDisplayName(family);
|
|
46
|
+
const name = `${scaleName} / ${step}`;
|
|
47
|
+
return primitiveNames.has(name) ? name : null;
|
|
48
|
+
}
|
|
49
|
+
const oneOffMatch = /^(?:new|oneOff)\.(.+)$/.exec(r);
|
|
50
|
+
if (oneOffMatch) {
|
|
51
|
+
const name = `One-off / ${oneOffMatch[1]}`;
|
|
52
|
+
return primitiveNames.has(name) ? name : null;
|
|
53
|
+
}
|
|
54
|
+
const whiteAlpha = /^whiteAlpha\.(.+)$/.exec(r);
|
|
55
|
+
if (whiteAlpha) {
|
|
56
|
+
const name = `WhiteAlpha / ${whiteAlpha[1]}`;
|
|
57
|
+
return primitiveNames.has(name) ? name : null;
|
|
58
|
+
}
|
|
59
|
+
const blackAlpha = /^blackAlpha\.(.+)$/.exec(r);
|
|
60
|
+
if (blackAlpha) {
|
|
61
|
+
const name = `BlackAlpha / ${blackAlpha[1]}`;
|
|
62
|
+
return primitiveNames.has(name) ? name : null;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function referenceToScaleName(ref) {
|
|
67
|
+
if (!ref || typeof ref !== "string") return null;
|
|
68
|
+
const r = ref.trim();
|
|
69
|
+
const scaleStep = /^([a-zA-Z]+)\.(\d+|[a-z]+)$/.exec(r);
|
|
70
|
+
if (scaleStep) return refScaleToFigmaDisplayName(scaleStep[1]);
|
|
71
|
+
const radixMatch = /^radix\.([a-zA-Z]+)\.(\d+)$/.exec(r);
|
|
72
|
+
if (radixMatch) return refScaleToFigmaDisplayName(radixMatch[1]);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function getReferencedScaleNames(storedColors) {
|
|
76
|
+
const names = /* @__PURE__ */ new Set();
|
|
77
|
+
const groupOrder = getColorGroupOrder(storedColors);
|
|
78
|
+
for (const groupName of groupOrder) {
|
|
79
|
+
const group = storedColors[groupName];
|
|
80
|
+
if (!group || typeof group !== "object") continue;
|
|
81
|
+
const keys = Object.keys(group).filter((k) => !k.startsWith("_") && k !== "governance");
|
|
82
|
+
for (const key of keys) {
|
|
83
|
+
const value = group[key];
|
|
84
|
+
if (value === void 0 || typeof value !== "object" || value === null) continue;
|
|
85
|
+
const theme = value;
|
|
86
|
+
const lightScale = theme.light?.reference ? referenceToScaleName(theme.light.reference) : null;
|
|
87
|
+
const darkScale = theme.dark?.reference ? referenceToScaleName(theme.dark.reference) : null;
|
|
88
|
+
if (lightScale) names.add(lightScale);
|
|
89
|
+
if (darkScale) names.add(darkScale);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return names;
|
|
93
|
+
}
|
|
94
|
+
function getFullScaleFromStored(storedColors, scaleName) {
|
|
95
|
+
const key = scaleName.toLowerCase();
|
|
96
|
+
if (key === "neutral" || key === "gray") {
|
|
97
|
+
const neutral = storedColors.neutral;
|
|
98
|
+
if (!neutral?.steps || typeof neutral.steps !== "object") return null;
|
|
99
|
+
const out = {};
|
|
100
|
+
for (const [step, stepValue] of Object.entries(neutral.steps)) {
|
|
101
|
+
const hex = stepValue?.hex;
|
|
102
|
+
if (typeof hex === "string" && /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/i.test(hex)) {
|
|
103
|
+
out[step] = hex.startsWith("#") ? hex : `#${hex}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
107
|
+
}
|
|
108
|
+
const customScales = storedColors.customScales;
|
|
109
|
+
if (Array.isArray(customScales)) {
|
|
110
|
+
const keyNorm = key.replace(/\s+/g, "");
|
|
111
|
+
const scale = customScales.find(
|
|
112
|
+
(s) => s.name?.toLowerCase() === key || s.name?.toLowerCase().replace(/\s+/g, "") === keyNorm
|
|
113
|
+
);
|
|
114
|
+
if (scale?.steps && typeof scale.steps === "object") {
|
|
115
|
+
const out = {};
|
|
116
|
+
for (const [step, stepValue] of Object.entries(scale.steps)) {
|
|
117
|
+
const hex = stepValue?.hex;
|
|
118
|
+
if (typeof hex === "string" && /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/i.test(hex)) {
|
|
119
|
+
out[step] = hex.startsWith("#") ? hex : `#${hex}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (Object.keys(out).length > 0) return out;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function buildSemanticRefMap(storedColors, primitiveNames) {
|
|
128
|
+
const out = {};
|
|
129
|
+
const groupOrder = getColorGroupOrder(storedColors);
|
|
130
|
+
for (const groupName of groupOrder) {
|
|
131
|
+
const group = storedColors[groupName];
|
|
132
|
+
if (!group || typeof group !== "object") continue;
|
|
133
|
+
const prefix = getGroupPrefix(storedColors, groupName);
|
|
134
|
+
const rowOrder = Array.isArray(group._rowOrder) ? group._rowOrder : void 0;
|
|
135
|
+
const keys = Array.isArray(rowOrder) ? rowOrder : Object.keys(group).filter((k) => !k.startsWith("_") && k !== "governance");
|
|
136
|
+
const toKebab = prefix === "action" || prefix === "brand" || prefix === "feedback";
|
|
137
|
+
for (const key of keys) {
|
|
138
|
+
const value = group[key];
|
|
139
|
+
if (value === void 0 || typeof value !== "object" || value === null) continue;
|
|
140
|
+
const theme = value;
|
|
141
|
+
let cssKey = key === "app" ? "page" : key;
|
|
142
|
+
if (toKebab) {
|
|
143
|
+
cssKey = String(key).replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
144
|
+
}
|
|
145
|
+
const fullKey = `${prefix}-${cssKey}`;
|
|
146
|
+
const lightRef = theme.light?.reference ? referenceToFigmaPrimitiveName(theme.light.reference, primitiveNames) : null;
|
|
147
|
+
const darkRef = theme.dark?.reference ? referenceToFigmaPrimitiveName(theme.dark.reference, primitiveNames) : null;
|
|
148
|
+
if (lightRef || darkRef) {
|
|
149
|
+
out[fullKey] = {};
|
|
150
|
+
if (lightRef) out[fullKey].Light = lightRef;
|
|
151
|
+
if (darkRef) out[fullKey].Dark = darkRef;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
function figmaColorNameWithGroup(key) {
|
|
158
|
+
if (key.includes("/")) {
|
|
159
|
+
const [group2, ...rest] = key.split("/");
|
|
160
|
+
const name2 = rest.join("/").trim();
|
|
161
|
+
if (!name2) return key;
|
|
162
|
+
const groupDisplay2 = group2.charAt(0).toUpperCase() + group2.slice(1).toLowerCase();
|
|
163
|
+
return `${groupDisplay2} / ${name2}`;
|
|
164
|
+
}
|
|
165
|
+
const firstDash = key.indexOf("-");
|
|
166
|
+
if (firstDash <= 0) return key;
|
|
167
|
+
const group = key.slice(0, firstDash);
|
|
168
|
+
const name = key.slice(firstDash + 1);
|
|
169
|
+
const groupDisplay = group.charAt(0).toUpperCase() + group.slice(1).toLowerCase();
|
|
170
|
+
return `${groupDisplay} / ${name}`;
|
|
171
|
+
}
|
|
172
|
+
var FIGMA_SHADOW_ORDER = {
|
|
173
|
+
none: 0,
|
|
174
|
+
xs: 1,
|
|
175
|
+
sm: 2,
|
|
176
|
+
md: 3,
|
|
177
|
+
lg: 4,
|
|
178
|
+
xl: 5,
|
|
179
|
+
"2xl": 6,
|
|
180
|
+
focus: 7
|
|
181
|
+
};
|
|
182
|
+
function tokenValueToNumber(s) {
|
|
183
|
+
if (typeof s !== "string" || !s.trim()) return 0;
|
|
184
|
+
const t = s.trim();
|
|
185
|
+
if (t.endsWith("rem")) {
|
|
186
|
+
const n2 = parseFloat(t.replace(/rem$/, ""));
|
|
187
|
+
return Number.isFinite(n2) ? Math.round(n2 * 16) : 0;
|
|
188
|
+
}
|
|
189
|
+
if (t.endsWith("px")) {
|
|
190
|
+
const n2 = parseFloat(t.replace(/px$/, ""));
|
|
191
|
+
return Number.isFinite(n2) ? Math.round(n2) : 0;
|
|
192
|
+
}
|
|
193
|
+
const n = parseFloat(t);
|
|
194
|
+
return Number.isFinite(n) ? n : 0;
|
|
195
|
+
}
|
|
196
|
+
function parseBoxShadowToFigmaEffect(shadowStr) {
|
|
197
|
+
const s = shadowStr.trim();
|
|
198
|
+
if (!s || s.toLowerCase() === "none") return null;
|
|
199
|
+
const parsePx = (x) => typeof x === "string" ? parseFloat(x.replace(/px$/i, "")) : NaN;
|
|
200
|
+
const colorMatch = s.match(/(rgba?\s*\([^)]+\)|#[0-9A-Fa-f]{3,8})\s*$/i);
|
|
201
|
+
const colorStr = colorMatch ? colorMatch[1].trim() : void 0;
|
|
202
|
+
const rest = (colorMatch ? s.slice(0, colorMatch.index) : s).trim();
|
|
203
|
+
const parts = rest ? rest.split(/\s+/) : [];
|
|
204
|
+
if (parts.length < 3) return null;
|
|
205
|
+
const offsetX = parsePx(parts[0]);
|
|
206
|
+
const offsetY = parsePx(parts[1]);
|
|
207
|
+
const blur = parsePx(parts[2]);
|
|
208
|
+
let spread = 0;
|
|
209
|
+
if (parts.length >= 4) spread = parsePx(parts[3]);
|
|
210
|
+
let r = 0, g = 0, b = 0, a = 0.1;
|
|
211
|
+
if (colorStr) {
|
|
212
|
+
const rgbaMatch = colorStr.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i);
|
|
213
|
+
if (rgbaMatch) {
|
|
214
|
+
r = Number(rgbaMatch[1]) / 255;
|
|
215
|
+
g = Number(rgbaMatch[2]) / 255;
|
|
216
|
+
b = Number(rgbaMatch[3]) / 255;
|
|
217
|
+
a = rgbaMatch[4] != null ? parseFloat(rgbaMatch[4]) : 1;
|
|
218
|
+
} else {
|
|
219
|
+
const hexMatch = colorStr.match(/^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/);
|
|
220
|
+
if (hexMatch) {
|
|
221
|
+
r = parseInt(hexMatch[1].slice(0, 2), 16) / 255;
|
|
222
|
+
g = parseInt(hexMatch[1].slice(2, 4), 16) / 255;
|
|
223
|
+
b = parseInt(hexMatch[1].slice(4, 6), 16) / 255;
|
|
224
|
+
a = hexMatch[2] ? parseInt(hexMatch[2], 16) / 255 : 0.1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (!Number.isFinite(offsetX) || !Number.isFinite(offsetY) || !Number.isFinite(blur)) return null;
|
|
229
|
+
return {
|
|
230
|
+
type: "DROP_SHADOW",
|
|
231
|
+
offset: { x: offsetX, y: offsetY },
|
|
232
|
+
radius: Math.max(0, blur),
|
|
233
|
+
spread: Number.isFinite(spread) ? spread : 0,
|
|
234
|
+
color: { r, g, b, a },
|
|
235
|
+
visible: true,
|
|
236
|
+
blendMode: "NORMAL"
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function parseBoxShadowToFigmaEffects(shadowStr) {
|
|
240
|
+
const s = (shadowStr || "").trim();
|
|
241
|
+
if (!s || s.toLowerCase() === "none") return [];
|
|
242
|
+
const out = [];
|
|
243
|
+
const segments = s.split(/\s*,\s*/);
|
|
244
|
+
for (const seg of segments) {
|
|
245
|
+
const effect = parseBoxShadowToFigmaEffect(seg.trim());
|
|
246
|
+
if (effect) out.push(effect);
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
function colorTo8DigitHex(color) {
|
|
251
|
+
if (!color || typeof color !== "string") return null;
|
|
252
|
+
const s = color.trim();
|
|
253
|
+
const rgbaMatch = s.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i);
|
|
254
|
+
if (rgbaMatch) {
|
|
255
|
+
const r = Math.max(0, Math.min(255, parseInt(rgbaMatch[1], 10)));
|
|
256
|
+
const g = Math.max(0, Math.min(255, parseInt(rgbaMatch[2], 10)));
|
|
257
|
+
const b = Math.max(0, Math.min(255, parseInt(rgbaMatch[3], 10)));
|
|
258
|
+
const a = rgbaMatch[4] != null ? Math.max(0, Math.min(1, parseFloat(rgbaMatch[4]))) : 1;
|
|
259
|
+
const aByte = Math.round(a * 255);
|
|
260
|
+
const hex = "#" + [r, g, b, aByte].map((n) => n.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
261
|
+
return hex;
|
|
262
|
+
}
|
|
263
|
+
const hexMatch = s.match(/^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/i);
|
|
264
|
+
if (hexMatch) {
|
|
265
|
+
const rgb = hexMatch[1];
|
|
266
|
+
const aa = hexMatch[2] ?? "FF";
|
|
267
|
+
return `#${rgb}${aa}`.toUpperCase();
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
function typesetKeyToFontFamilyRole(key) {
|
|
272
|
+
const prefix = (key.split("-")[0] ?? key).toLowerCase();
|
|
273
|
+
if (prefix === "display" || prefix.startsWith("display")) return "display";
|
|
274
|
+
if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
|
|
275
|
+
if (prefix === "mono" || prefix.startsWith("mono")) return "mono";
|
|
276
|
+
if (prefix.startsWith("body")) return "body";
|
|
277
|
+
return "body";
|
|
278
|
+
}
|
|
279
|
+
function buildFigmaPayloadsFromDS(data) {
|
|
280
|
+
const tokens = data.tokens;
|
|
281
|
+
const colors = tokens?.colors;
|
|
282
|
+
const typography = tokens?.typography;
|
|
283
|
+
const hexRe = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
|
|
284
|
+
const modes = [];
|
|
285
|
+
if (colors?.modes) {
|
|
286
|
+
const light = colors.modes.light ?? {};
|
|
287
|
+
const dark = colors.modes.dark ?? {};
|
|
288
|
+
if (Object.keys(light).length > 0) modes.push("Light");
|
|
289
|
+
if (Object.keys(dark).length > 0 && !modes.includes("Dark")) modes.push("Dark");
|
|
290
|
+
}
|
|
291
|
+
if (modes.length === 0) modes.push("Light");
|
|
292
|
+
const primitiveModes = ["Value"];
|
|
293
|
+
const dsName = data.meta?.name;
|
|
294
|
+
const collectionPrefix = dsName ? `${dsName} ` : "";
|
|
295
|
+
const referencedScaleNames = data.storedColors ? getReferencedScaleNames(data.storedColors) : /* @__PURE__ */ new Set();
|
|
296
|
+
const primitiveVariables = [];
|
|
297
|
+
const primitiveNames = /* @__PURE__ */ new Set();
|
|
298
|
+
const oneOffHexToFigmaName = /* @__PURE__ */ new Map();
|
|
299
|
+
const alphaScales = colors?.alphaScales;
|
|
300
|
+
if (alphaScales?.whiteAlpha && typeof alphaScales.whiteAlpha === "object") {
|
|
301
|
+
for (const [step, value] of Object.entries(alphaScales.whiteAlpha)) {
|
|
302
|
+
if (typeof value !== "string") continue;
|
|
303
|
+
const hex = colorTo8DigitHex(value);
|
|
304
|
+
if (!hex || !hexRe.test(hex)) continue;
|
|
305
|
+
const figmaName = `WhiteAlpha / ${step}`;
|
|
306
|
+
if (primitiveNames.has(figmaName)) continue;
|
|
307
|
+
primitiveNames.add(figmaName);
|
|
308
|
+
const values = {};
|
|
309
|
+
for (const m of primitiveModes) values[m] = hex;
|
|
310
|
+
primitiveVariables.push({ name: figmaName, values });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (alphaScales?.blackAlpha && typeof alphaScales.blackAlpha === "object") {
|
|
314
|
+
for (const [step, value] of Object.entries(alphaScales.blackAlpha)) {
|
|
315
|
+
if (typeof value !== "string") continue;
|
|
316
|
+
const hex = colorTo8DigitHex(value);
|
|
317
|
+
if (!hex || !hexRe.test(hex)) continue;
|
|
318
|
+
const figmaName = `BlackAlpha / ${step}`;
|
|
319
|
+
if (primitiveNames.has(figmaName)) continue;
|
|
320
|
+
primitiveNames.add(figmaName);
|
|
321
|
+
const values = {};
|
|
322
|
+
for (const m of primitiveModes) values[m] = hex;
|
|
323
|
+
primitiveVariables.push({ name: figmaName, values });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (colors?.scales && typeof colors.scales === "object") {
|
|
327
|
+
for (const [scaleName, steps] of Object.entries(colors.scales)) {
|
|
328
|
+
if (!steps || typeof steps !== "object") continue;
|
|
329
|
+
let groupDisplay = scaleName.charAt(0).toUpperCase() + scaleName.slice(1);
|
|
330
|
+
if (scaleName.toLowerCase() === "neutral" && data.storedColors) {
|
|
331
|
+
const neutral = data.storedColors.neutral;
|
|
332
|
+
if (neutral?.baseHueFamily) {
|
|
333
|
+
groupDisplay = refScaleToFigmaDisplayName(neutral.baseHueFamily);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (const [step, hexVal] of Object.entries(steps)) {
|
|
337
|
+
if (typeof hexVal !== "string") continue;
|
|
338
|
+
const hex = colorTo8DigitHex(hexVal) ?? (hexRe.test(hexVal) ? hexVal : null);
|
|
339
|
+
if (!hex || !hexRe.test(hex)) continue;
|
|
340
|
+
const figmaName = `${groupDisplay} / ${step}`;
|
|
341
|
+
if (primitiveNames.has(figmaName)) continue;
|
|
342
|
+
primitiveNames.add(figmaName);
|
|
343
|
+
const values = {};
|
|
344
|
+
for (const m of primitiveModes) values[m] = hex;
|
|
345
|
+
primitiveVariables.push({ name: figmaName, values });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const radixVariables = [];
|
|
350
|
+
const radixCollectionName = `${collectionPrefix}Colors Radix`;
|
|
351
|
+
for (const scaleName of referencedScaleNames) {
|
|
352
|
+
if (primitiveNames.has(`${scaleName} / 50`) || primitiveNames.has(`${scaleName} / 100`)) continue;
|
|
353
|
+
const fromStored = data.storedColors ? getFullScaleFromStored(data.storedColors, scaleName) : null;
|
|
354
|
+
const scaleData = fromStored ?? data.getRadixScale?.(scaleName) ?? data.resolvedRadixScales?.[scaleName] ?? null;
|
|
355
|
+
if (!scaleData) continue;
|
|
356
|
+
for (const [step, hexVal] of Object.entries(scaleData)) {
|
|
357
|
+
const figmaName = `${scaleName} / ${step}`;
|
|
358
|
+
if (primitiveNames.has(figmaName)) continue;
|
|
359
|
+
const hex = colorTo8DigitHex(hexVal) ?? (hexRe.test(hexVal) ? hexVal : null);
|
|
360
|
+
if (!hex || !hexRe.test(hex)) continue;
|
|
361
|
+
primitiveNames.add(figmaName);
|
|
362
|
+
const values = {};
|
|
363
|
+
for (const m of primitiveModes) values[m] = hex;
|
|
364
|
+
primitiveVariables.push({ name: figmaName, values });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (colors?.oneOffs && Array.isArray(colors.oneOffs)) {
|
|
368
|
+
for (const oneOff of colors.oneOffs) {
|
|
369
|
+
if (!oneOff || typeof oneOff !== "object" || typeof oneOff.hex !== "string") continue;
|
|
370
|
+
const hex = colorTo8DigitHex(oneOff.hex) ?? (hexRe.test(oneOff.hex) ? oneOff.hex : null);
|
|
371
|
+
if (!hex || !hexRe.test(hex)) continue;
|
|
372
|
+
const name = typeof oneOff.name === "string" && oneOff.name ? oneOff.name : oneOff.id ?? "unnamed";
|
|
373
|
+
const figmaName = `One-off / ${name}`;
|
|
374
|
+
if (primitiveNames.has(figmaName)) continue;
|
|
375
|
+
primitiveNames.add(figmaName);
|
|
376
|
+
const values = {};
|
|
377
|
+
for (const m of primitiveModes) values[m] = hex;
|
|
378
|
+
primitiveVariables.push({ name: figmaName, values });
|
|
379
|
+
const normalizedHex = hex.replace(/^#/, "").toUpperCase();
|
|
380
|
+
const key8 = normalizedHex.length === 6 ? normalizedHex + "FF" : normalizedHex;
|
|
381
|
+
oneOffHexToFigmaName.set(key8, figmaName);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const semanticRefMap = data.storedColors && primitiveNames.size > 0 ? buildSemanticRefMap(data.storedColors, primitiveNames) : {};
|
|
385
|
+
const semanticVariables = [];
|
|
386
|
+
const semanticNames = /* @__PURE__ */ new Set();
|
|
387
|
+
const primitivesCollectionName = `${collectionPrefix}Colors Primitives`;
|
|
388
|
+
if (colors?.modes) {
|
|
389
|
+
const light = colors.modes.light ?? {};
|
|
390
|
+
const dark = colors.modes.dark ?? {};
|
|
391
|
+
const orderedKeys = [...Object.keys(light)];
|
|
392
|
+
for (const k of Object.keys(dark)) {
|
|
393
|
+
if (!orderedKeys.includes(k)) orderedKeys.push(k);
|
|
394
|
+
}
|
|
395
|
+
for (const key of orderedKeys) {
|
|
396
|
+
const lightVal = light[key];
|
|
397
|
+
const darkVal = dark[key];
|
|
398
|
+
const lightHex = typeof lightVal === "string" ? colorTo8DigitHex(lightVal) ?? (hexRe.test(lightVal) ? lightVal : null) : null;
|
|
399
|
+
if (lightHex && hexRe.test(lightHex)) {
|
|
400
|
+
const figmaName = figmaColorNameWithGroup(key);
|
|
401
|
+
if (semanticNames.has(figmaName)) continue;
|
|
402
|
+
semanticNames.add(figmaName);
|
|
403
|
+
const darkHex = typeof darkVal === "string" ? colorTo8DigitHex(darkVal) ?? (hexRe.test(darkVal) ? darkVal : null) : null;
|
|
404
|
+
const refs = semanticRefMap[key];
|
|
405
|
+
const values = {
|
|
406
|
+
...modes.includes("Light") && { Light: lightHex },
|
|
407
|
+
...modes.includes("Dark") && { Dark: darkHex && hexRe.test(darkHex) ? darkHex : lightHex }
|
|
408
|
+
};
|
|
409
|
+
const aliasByMode = {};
|
|
410
|
+
for (const m of modes) {
|
|
411
|
+
const aliasFromRef = m === "Light" ? refs?.Light : refs?.Dark;
|
|
412
|
+
if (aliasFromRef && primitiveNames.has(aliasFromRef)) {
|
|
413
|
+
aliasByMode[m] = aliasFromRef;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const hexForMode = m === "Light" ? lightHex : darkHex && hexRe.test(darkHex) ? darkHex : lightHex;
|
|
417
|
+
const norm = hexForMode.replace(/^#/, "").toUpperCase();
|
|
418
|
+
const key8 = norm.length === 6 ? norm + "FF" : norm;
|
|
419
|
+
const oneOffAlias = oneOffHexToFigmaName.get(key8);
|
|
420
|
+
if (oneOffAlias) {
|
|
421
|
+
aliasByMode[m] = oneOffAlias;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
semanticVariables.push({
|
|
425
|
+
name: figmaName,
|
|
426
|
+
values,
|
|
427
|
+
...Object.keys(aliasByMode).length > 0 ? { aliasByMode } : {}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (colors?.static?.brand && typeof colors.static.brand === "object") {
|
|
433
|
+
for (const [key, hex] of Object.entries(colors.static.brand)) {
|
|
434
|
+
if (typeof hex !== "string" || !hexRe.test(hex)) continue;
|
|
435
|
+
const figmaName = figmaColorNameWithGroup(`brand/${key}`);
|
|
436
|
+
if (semanticNames.has(figmaName)) continue;
|
|
437
|
+
semanticNames.add(figmaName);
|
|
438
|
+
const values = {};
|
|
439
|
+
for (const m of modes) values[m] = hex;
|
|
440
|
+
semanticVariables.push({ name: figmaName, values });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const colorVariableCollections = [];
|
|
444
|
+
if (primitiveVariables.length > 0) {
|
|
445
|
+
colorVariableCollections.push({
|
|
446
|
+
collectionName: primitivesCollectionName,
|
|
447
|
+
modes: primitiveModes,
|
|
448
|
+
variables: primitiveVariables,
|
|
449
|
+
applyScopes: false
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
if (radixVariables.length > 0) {
|
|
453
|
+
colorVariableCollections.push({
|
|
454
|
+
collectionName: radixCollectionName,
|
|
455
|
+
modes,
|
|
456
|
+
variables: radixVariables,
|
|
457
|
+
applyScopes: false
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (semanticVariables.length > 0) {
|
|
461
|
+
const primitiveCollections = [];
|
|
462
|
+
if (primitiveVariables.length > 0) primitiveCollections.push(primitivesCollectionName);
|
|
463
|
+
if (radixVariables.length > 0) primitiveCollections.push(radixCollectionName);
|
|
464
|
+
colorVariableCollections.push({
|
|
465
|
+
collectionName: `${collectionPrefix}Colors Semantic`,
|
|
466
|
+
modes,
|
|
467
|
+
variables: semanticVariables,
|
|
468
|
+
applyScopes: true,
|
|
469
|
+
...primitiveCollections.length > 1 ? { primitiveCollectionNames: primitiveCollections } : primitiveCollections.length === 1 ? { primitiveCollectionName: primitiveCollections[0] } : {}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
const paintStyles = [];
|
|
473
|
+
const textStyles = [];
|
|
474
|
+
const sizeToPx = (val, basePx = 16) => {
|
|
475
|
+
if (typeof val === "number") return Math.round(val);
|
|
476
|
+
const s = String(val).trim();
|
|
477
|
+
const pxMatch = s.match(/^([\d.]+)\s*px$/i);
|
|
478
|
+
if (pxMatch) return Math.round(parseFloat(pxMatch[1]));
|
|
479
|
+
const remMatch = s.match(/^([\d.]+)\s*rem$/i);
|
|
480
|
+
if (remMatch) return Math.round(parseFloat(remMatch[1]) * basePx);
|
|
481
|
+
const n = parseFloat(s);
|
|
482
|
+
if (Number.isFinite(n)) return n <= 0 ? basePx : n < 50 ? Math.round(n * basePx) : Math.round(n);
|
|
483
|
+
return basePx;
|
|
484
|
+
};
|
|
485
|
+
const letterSpacingToPx = (val, fontSizePx) => {
|
|
486
|
+
if (val === void 0 || val === null) return void 0;
|
|
487
|
+
if (typeof val === "number") return Math.round(val);
|
|
488
|
+
const s = String(val).trim();
|
|
489
|
+
const pxMatch = s.match(/^([-\d.]+)\s*px$/i);
|
|
490
|
+
if (pxMatch) return Math.round(parseFloat(pxMatch[1]));
|
|
491
|
+
const emMatch = s.match(/^([-\d.]+)\s*em$/i);
|
|
492
|
+
if (emMatch) return Math.round(parseFloat(emMatch[1]) * fontSizePx);
|
|
493
|
+
const n = parseFloat(s);
|
|
494
|
+
return Number.isFinite(n) ? Math.round(n) : void 0;
|
|
495
|
+
};
|
|
496
|
+
const firstFont = (obj) => {
|
|
497
|
+
if (typeof obj === "string") {
|
|
498
|
+
const primary = obj.split(",")[0].trim().replace(/^['"]|['"]$/g, "");
|
|
499
|
+
return primary || "Inter";
|
|
500
|
+
}
|
|
501
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
502
|
+
const v = obj.body ?? obj.heading ?? obj.display ?? Object.values(obj)[0];
|
|
503
|
+
return firstFont(v);
|
|
504
|
+
}
|
|
505
|
+
return "Inter";
|
|
506
|
+
};
|
|
507
|
+
const toFontFamilyString = (val) => {
|
|
508
|
+
if (typeof val === "string") {
|
|
509
|
+
const s = val.trim().replace(/^["']|["']$/g, "");
|
|
510
|
+
return s || "Inter";
|
|
511
|
+
}
|
|
512
|
+
return firstFont(val);
|
|
513
|
+
};
|
|
514
|
+
const fontFamilyMap = typography?.fontFamily ?? {};
|
|
515
|
+
const defaultFontFamily = typography ? firstFont(typography.fontFamily ?? "Inter") : "Inter";
|
|
516
|
+
const fontSizeMap = typography?.fontSize;
|
|
517
|
+
const fontWeightMap = typography?.fontWeight;
|
|
518
|
+
const lineHeightMap = typography?.lineHeight;
|
|
519
|
+
const letterSpacingMap = typography?.letterSpacing;
|
|
520
|
+
const textTransformMap = typography?.textTransform;
|
|
521
|
+
const textDecorationMap = typography?.textDecoration;
|
|
522
|
+
if (fontSizeMap && typeof fontSizeMap === "object" && Object.keys(fontSizeMap).length > 0) {
|
|
523
|
+
for (const [key, sizeVal] of Object.entries(fontSizeMap)) {
|
|
524
|
+
const fontSize = sizeToPx(sizeVal);
|
|
525
|
+
if (fontSize <= 0) continue;
|
|
526
|
+
const role = typesetKeyToFontFamilyRole(key);
|
|
527
|
+
const fontFamily = toFontFamilyString(
|
|
528
|
+
fontFamilyMap[role] ?? fontFamilyMap.body ?? fontFamilyMap.heading ?? fontFamilyMap.display ?? defaultFontFamily
|
|
529
|
+
);
|
|
530
|
+
const lh = lineHeightMap && typeof lineHeightMap === "object" ? lineHeightMap[key] : void 0;
|
|
531
|
+
const weight = fontWeightMap && typeof fontWeightMap === "object" ? fontWeightMap[key] : void 0;
|
|
532
|
+
const fontWeight = weight != null ? String(weight) : "400";
|
|
533
|
+
const letterSpacingPx = letterSpacingToPx(
|
|
534
|
+
letterSpacingMap && typeof letterSpacingMap === "object" ? letterSpacingMap[key] : void 0,
|
|
535
|
+
fontSize
|
|
536
|
+
);
|
|
537
|
+
const textTransform = textTransformMap && typeof textTransformMap === "object" ? textTransformMap[key] : void 0;
|
|
538
|
+
const textDecoration = textDecorationMap && typeof textDecorationMap === "object" ? textDecorationMap[key] : void 0;
|
|
539
|
+
const namePart = key.replace(/-/g, " / ");
|
|
540
|
+
const style = {
|
|
541
|
+
name: namePart.startsWith("Typography") ? namePart : `Typography / ${namePart}`,
|
|
542
|
+
fontFamily,
|
|
543
|
+
fontWeight,
|
|
544
|
+
fontSize,
|
|
545
|
+
lineHeightUnit: "PERCENT",
|
|
546
|
+
letterSpacingUnit: "PIXELS",
|
|
547
|
+
...letterSpacingPx !== void 0 && letterSpacingPx !== 0 ? { letterSpacingValue: letterSpacingPx } : {}
|
|
548
|
+
};
|
|
549
|
+
if (lh != null && typeof lh === "number" && lh > 0) {
|
|
550
|
+
style.lineHeightValue = lh >= 10 ? Math.round(lh / fontSize * 100) : Math.round(lh * 100);
|
|
551
|
+
} else {
|
|
552
|
+
style.lineHeightValue = 150;
|
|
553
|
+
}
|
|
554
|
+
if (textTransform === "uppercase") style.textCase = "UPPER";
|
|
555
|
+
else if (textTransform === "lowercase") style.textCase = "LOWER";
|
|
556
|
+
else if (textTransform === "capitalize") style.textCase = "TITLE";
|
|
557
|
+
else style.textCase = "ORIGINAL";
|
|
558
|
+
if (textDecoration === "underline") style.textDecoration = "UNDERLINE";
|
|
559
|
+
else style.textDecoration = "NONE";
|
|
560
|
+
textStyles.push(style);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const textStylesMap = typography?.textStyles;
|
|
564
|
+
if (textStyles.length === 0 && textStylesMap && typeof textStylesMap === "object") {
|
|
565
|
+
for (const [styleName, style] of Object.entries(textStylesMap)) {
|
|
566
|
+
if (!style || typeof style !== "object") continue;
|
|
567
|
+
const fontSize = sizeToPx(style.fontSize ?? "1rem");
|
|
568
|
+
const lhStr = style.lineHeight;
|
|
569
|
+
const lineHeightUnitless = lhStr != null ? lhStr.endsWith("%") ? parseFloat(lhStr) / 100 : sizeToPx(lhStr) / fontSize : 1.5;
|
|
570
|
+
const payload = {
|
|
571
|
+
name: styleName.startsWith("Typography") ? styleName : `Typography / ${styleName.replace(/\//g, " / ")}`,
|
|
572
|
+
fontFamily: defaultFontFamily,
|
|
573
|
+
fontWeight: String(style.fontWeight ?? "400"),
|
|
574
|
+
fontSize,
|
|
575
|
+
lineHeightUnit: "PERCENT",
|
|
576
|
+
lineHeightValue: Math.round((Number.isFinite(lineHeightUnitless) ? lineHeightUnitless : 1.5) * 100),
|
|
577
|
+
letterSpacingUnit: "PIXELS",
|
|
578
|
+
textCase: "ORIGINAL",
|
|
579
|
+
textDecoration: "NONE"
|
|
580
|
+
};
|
|
581
|
+
textStyles.push(payload);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
textStyles.sort((a, b) => {
|
|
585
|
+
if (a.fontSize !== b.fontSize) return a.fontSize - b.fontSize;
|
|
586
|
+
return (a.name || "").localeCompare(b.name || "");
|
|
587
|
+
});
|
|
588
|
+
const numberVariableCollections = [];
|
|
589
|
+
const spacing = tokens?.spacing;
|
|
590
|
+
if (spacing?.scale && typeof spacing.scale === "object") {
|
|
591
|
+
const vars = [];
|
|
592
|
+
for (const [key, val] of Object.entries(spacing.scale)) {
|
|
593
|
+
const n = tokenValueToNumber(val);
|
|
594
|
+
if (n >= 0) vars.push({ name: `Spacing / ${key}`, value: n });
|
|
595
|
+
}
|
|
596
|
+
vars.sort((a, b) => a.value - b.value);
|
|
597
|
+
if (vars.length > 0)
|
|
598
|
+
numberVariableCollections.push({
|
|
599
|
+
collectionName: `${collectionPrefix}Spacing`,
|
|
600
|
+
categoryKey: "Spacing",
|
|
601
|
+
variables: vars,
|
|
602
|
+
scopes: ["GAP"]
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
const radius = tokens?.radius;
|
|
606
|
+
if (radius?.scale && typeof radius.scale === "object") {
|
|
607
|
+
const vars = [];
|
|
608
|
+
for (const [key, val] of Object.entries(radius.scale)) {
|
|
609
|
+
const n = tokenValueToNumber(val);
|
|
610
|
+
if (n >= 0) vars.push({ name: `Radius / ${key}`, value: n });
|
|
611
|
+
}
|
|
612
|
+
vars.sort((a, b) => a.value - b.value);
|
|
613
|
+
if (vars.length > 0)
|
|
614
|
+
numberVariableCollections.push({
|
|
615
|
+
collectionName: `${collectionPrefix}Radius`,
|
|
616
|
+
categoryKey: "Radius",
|
|
617
|
+
variables: vars,
|
|
618
|
+
scopes: ["CORNER_RADIUS"]
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
const borders = tokens?.borders;
|
|
622
|
+
if (borders?.width && typeof borders.width === "object") {
|
|
623
|
+
const vars = [];
|
|
624
|
+
for (const [key, val] of Object.entries(borders.width)) {
|
|
625
|
+
const n = tokenValueToNumber(val);
|
|
626
|
+
if (n >= 0) vars.push({ name: `Borders / ${key}`, value: n });
|
|
627
|
+
}
|
|
628
|
+
vars.sort((a, b) => a.value - b.value);
|
|
629
|
+
if (vars.length > 0)
|
|
630
|
+
numberVariableCollections.push({
|
|
631
|
+
collectionName: `${collectionPrefix}Borders`,
|
|
632
|
+
categoryKey: "Borders",
|
|
633
|
+
variables: vars,
|
|
634
|
+
scopes: ["STROKE_FLOAT"]
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
const sizing = tokens?.sizing;
|
|
638
|
+
const sizingVariables = [];
|
|
639
|
+
if (sizing?.height && typeof sizing.height === "object") {
|
|
640
|
+
for (const [key, val] of Object.entries(sizing.height)) {
|
|
641
|
+
const n = tokenValueToNumber(val);
|
|
642
|
+
if (n >= 0) sizingVariables.push({ name: `Height / ${key}`, value: n });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (sizing?.icon && typeof sizing.icon === "object") {
|
|
646
|
+
for (const [key, val] of Object.entries(sizing.icon)) {
|
|
647
|
+
const n = tokenValueToNumber(val);
|
|
648
|
+
if (n >= 0) sizingVariables.push({ name: `Icon / ${key}`, value: n });
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
sizingVariables.sort((a, b) => a.value - b.value);
|
|
652
|
+
if (sizingVariables.length > 0) {
|
|
653
|
+
numberVariableCollections.push({
|
|
654
|
+
collectionName: `${collectionPrefix}Sizing`,
|
|
655
|
+
categoryKey: "Sizing",
|
|
656
|
+
variables: sizingVariables,
|
|
657
|
+
scopes: ["WIDTH_HEIGHT"]
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
const layout = tokens?.layout;
|
|
661
|
+
if (layout?.breakpoint && typeof layout.breakpoint === "object") {
|
|
662
|
+
const vars = [];
|
|
663
|
+
for (const [key, val] of Object.entries(layout.breakpoint)) {
|
|
664
|
+
const n = tokenValueToNumber(val);
|
|
665
|
+
if (n >= 0) vars.push({ name: `Breakpoint / ${key}`, value: n });
|
|
666
|
+
}
|
|
667
|
+
vars.sort((a, b) => a.value - b.value);
|
|
668
|
+
if (vars.length > 0)
|
|
669
|
+
numberVariableCollections.push({
|
|
670
|
+
collectionName: `${collectionPrefix}Layout`,
|
|
671
|
+
categoryKey: "Layout",
|
|
672
|
+
variables: vars,
|
|
673
|
+
scopes: ["WIDTH_HEIGHT"]
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
const effectStyles = [];
|
|
677
|
+
const shadows = tokens?.shadows;
|
|
678
|
+
if (shadows?.elevation && typeof shadows.elevation === "object") {
|
|
679
|
+
for (const [key, val] of Object.entries(shadows.elevation)) {
|
|
680
|
+
if (typeof val !== "string") continue;
|
|
681
|
+
const effects = parseBoxShadowToFigmaEffects(val);
|
|
682
|
+
if (effects.length > 0) effectStyles.push({ name: `Shadow / ${key}`, effects });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (shadows?.focus && typeof shadows.focus === "string") {
|
|
686
|
+
const effects = parseBoxShadowToFigmaEffects(shadows.focus);
|
|
687
|
+
if (effects.length > 0) effectStyles.push({ name: "Shadow / focus", effects });
|
|
688
|
+
}
|
|
689
|
+
effectStyles.sort((a, b) => {
|
|
690
|
+
const nameA = a.name.startsWith("Shadow / ") ? a.name.slice(9) : a.name;
|
|
691
|
+
const nameB = b.name.startsWith("Shadow / ") ? b.name.slice(9) : b.name;
|
|
692
|
+
const orderA = FIGMA_SHADOW_ORDER[nameA] ?? 100;
|
|
693
|
+
const orderB = FIGMA_SHADOW_ORDER[nameB] ?? 100;
|
|
694
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
695
|
+
return nameA.localeCompare(nameB);
|
|
696
|
+
});
|
|
697
|
+
return {
|
|
698
|
+
colorVariableCollections,
|
|
699
|
+
paintStyles,
|
|
700
|
+
textStyles,
|
|
701
|
+
numberVariableCollections,
|
|
702
|
+
effectStyles
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
var FIGMA_DESIGN_CATALOG = {
|
|
706
|
+
// ---- Methods ----------------------------------------------------------
|
|
707
|
+
methods: [
|
|
708
|
+
// Query
|
|
709
|
+
{
|
|
710
|
+
method: "get_document_info",
|
|
711
|
+
category: "query",
|
|
712
|
+
description: "Returns basic document information (name, pages).",
|
|
713
|
+
params: []
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
method: "get_selection",
|
|
717
|
+
category: "query",
|
|
718
|
+
description: "Returns the current selection in the Figma file.",
|
|
719
|
+
params: []
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
method: "get_node_info",
|
|
723
|
+
category: "query",
|
|
724
|
+
description: "Returns detailed info about a specific node.",
|
|
725
|
+
params: [
|
|
726
|
+
{ name: "nodeId", type: "string", required: true, description: "Figma node ID" }
|
|
727
|
+
]
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
method: "get_figma_variables_and_styles",
|
|
731
|
+
category: "query",
|
|
732
|
+
description: "Returns all local variable collections, text styles, effect styles, and paint styles in the file.",
|
|
733
|
+
params: []
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
method: "get_figma_library_variables",
|
|
737
|
+
category: "query",
|
|
738
|
+
description: "Returns library (team/external) variable collections available to the file.",
|
|
739
|
+
params: []
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
method: "get_design_screenshot",
|
|
743
|
+
category: "query",
|
|
744
|
+
description: "Returns a base64 PNG screenshot of a frame.",
|
|
745
|
+
params: [
|
|
746
|
+
{ name: "frameId", type: "string", required: true, description: "Frame node ID" },
|
|
747
|
+
{ name: "scale", type: "number", required: false, description: "Export scale 1-4 (default 1)" }
|
|
748
|
+
]
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
method: "list_local_components",
|
|
752
|
+
category: "query",
|
|
753
|
+
description: "Lists all local components in the current file page. Returns componentId, name, and description. Use the returned componentId with design_create_component_instance.",
|
|
754
|
+
params: []
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
method: "get_component_catalog",
|
|
758
|
+
category: "query",
|
|
759
|
+
description: "Returns the curated design assets catalog: library components, library styles, library variables, local components, local styles, and local variables. Only enabled (non-excluded) items are returned. Use component keys with design_create_component_instance's componentKey param. Optionally filter by name with the query param.",
|
|
760
|
+
params: [
|
|
761
|
+
{ name: "query", type: "string", required: false, description: "Filter components by name (case-insensitive substring match)" }
|
|
762
|
+
]
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
method: "get_component_text_layers",
|
|
766
|
+
category: "query",
|
|
767
|
+
description: "Scans a component (library or local) and returns all visible text layers inside it. Returns an array of { name, characters } for each text node. Use this to discover the exact text node names before calling design_create_component_instance with textOverrides. Provide componentKey (library) or componentId (local) or nodeId (existing instance on canvas).",
|
|
768
|
+
params: [
|
|
769
|
+
{ name: "componentKey", type: "string", required: false, description: "Library component key to scan" },
|
|
770
|
+
{ name: "componentId", type: "string", required: false, description: "Local component ID to scan" },
|
|
771
|
+
{ name: "nodeId", type: "string", required: false, description: "Existing instance or component node ID on canvas" }
|
|
772
|
+
]
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
method: "get_variable_collection_modes",
|
|
776
|
+
category: "query",
|
|
777
|
+
description: "Returns available modes for a variable collection.",
|
|
778
|
+
params: [
|
|
779
|
+
{ name: "collectionId", type: "string", required: true, description: "Variable collection ID" }
|
|
780
|
+
]
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
method: "get_frame_variable_mode",
|
|
784
|
+
category: "query",
|
|
785
|
+
description: "Returns the current variable mode set on a frame for a collection.",
|
|
786
|
+
params: [
|
|
787
|
+
{ name: "frameId", type: "string", required: true, description: "Frame node ID" },
|
|
788
|
+
{ name: "collectionId", type: "string", required: true, description: "Variable collection ID" }
|
|
789
|
+
]
|
|
790
|
+
},
|
|
791
|
+
// Sync (token push)
|
|
792
|
+
{
|
|
793
|
+
method: "create_color_variables",
|
|
794
|
+
category: "sync",
|
|
795
|
+
description: "Create or update a color variable collection (primitives or semantics with light/dark modes).",
|
|
796
|
+
params: [
|
|
797
|
+
{ name: "collectionName", type: "string", required: true, description: "Collection name" },
|
|
798
|
+
{ name: "modes", type: "array", required: true, description: "Mode names (e.g. ['Light','Dark'])" },
|
|
799
|
+
{ name: "variables", type: "array", required: true, description: "Array of color variables" },
|
|
800
|
+
{ name: "applyScopes", type: "boolean", required: false, description: "Apply scopes to variables (true for semantics)" },
|
|
801
|
+
{ name: "primitiveCollectionName", type: "string", required: false, description: "Primitives collection for alias resolution" },
|
|
802
|
+
{ name: "primitiveCollectionNames", type: "array", required: false, description: "Multiple primitives collections for alias resolution" }
|
|
803
|
+
]
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
method: "create_paint_styles",
|
|
807
|
+
category: "sync",
|
|
808
|
+
description: "Create or update paint styles.",
|
|
809
|
+
params: [
|
|
810
|
+
{ name: "styles", type: "array", required: true, description: "Array of { name, color } paint styles" }
|
|
811
|
+
]
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
method: "create_text_styles",
|
|
815
|
+
category: "sync",
|
|
816
|
+
description: "Create or update text styles from typography tokens.",
|
|
817
|
+
params: [
|
|
818
|
+
{ name: "styles", type: "array", required: true, description: "Array of text style payloads" }
|
|
819
|
+
]
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
method: "create_number_variables",
|
|
823
|
+
category: "sync",
|
|
824
|
+
description: "Create or update a number variable collection (spacing, radius, borders, sizing, breakpoints).",
|
|
825
|
+
params: [
|
|
826
|
+
{ name: "collectionName", type: "string", required: true, description: "Collection name" },
|
|
827
|
+
{ name: "categoryKey", type: "string", required: true, description: "Token category key" },
|
|
828
|
+
{ name: "variables", type: "array", required: true, description: "Array of { name, value } number variables" },
|
|
829
|
+
{ name: "scopes", type: "array", required: true, description: "Figma variable scopes" }
|
|
830
|
+
]
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
method: "create_effect_styles",
|
|
834
|
+
category: "sync",
|
|
835
|
+
description: "Create or update effect styles (shadows).",
|
|
836
|
+
params: [
|
|
837
|
+
{ name: "styles", type: "array", required: true, description: "Array of { name, effects } effect styles" }
|
|
838
|
+
]
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
method: "apply_fill_to_selection",
|
|
842
|
+
category: "sync",
|
|
843
|
+
description: "Apply a fill color to the current Figma selection.",
|
|
844
|
+
params: [
|
|
845
|
+
{ name: "variableId", type: "string", required: false, description: "Variable ID to apply" },
|
|
846
|
+
{ name: "paintStyleId", type: "string", required: false, description: "Paint style ID to apply" }
|
|
847
|
+
]
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
method: "apply_matching_styles_to_selection",
|
|
851
|
+
category: "styling",
|
|
852
|
+
description: "Apply matching or closest variables and styles to the current Figma selection by color distance.",
|
|
853
|
+
params: [
|
|
854
|
+
{ name: "strategy", type: "string", required: false, description: "Match strategy", enum: ["closest", "variable"] },
|
|
855
|
+
{ name: "applyFills", type: "boolean", required: false, description: "Apply matching to fill colors (default true)" },
|
|
856
|
+
{ name: "applyStrokes", type: "boolean", required: false, description: "Apply matching to stroke colors (default true)" },
|
|
857
|
+
{ name: "applyTextStyles", type: "boolean", required: false, description: "Apply closest text style (default true)" },
|
|
858
|
+
{ name: "applyEffects", type: "boolean", required: false, description: "Apply closest effect style (default true)" },
|
|
859
|
+
{ name: "applyCornerRadius", type: "boolean", required: false, description: "Bind closest corner radius variable (default true)" }
|
|
860
|
+
]
|
|
861
|
+
},
|
|
862
|
+
// Create (design)
|
|
863
|
+
{
|
|
864
|
+
method: "design_create_frame",
|
|
865
|
+
category: "create",
|
|
866
|
+
description: "Creates a frame. When parentId is omitted, creates a top-level ROOT frame on the canvas with vertical auto-layout (FIXED size, clipped, auto-positioned next to existing content, selected and scrolled into view). When parentId is provided, creates a child frame inside that parent. USE FRAMES for all containers, sections, columns, cards, buttons, inputs \u2014 anything that holds children or needs auto-layout.",
|
|
867
|
+
params: [
|
|
868
|
+
{ name: "parentId", type: "string", required: false, description: "Parent node name or ID. Omit to create a top-level root frame." },
|
|
869
|
+
{ name: "name", type: "string", required: false, description: "Node name (used for ID resolution in later steps)" },
|
|
870
|
+
{ name: "width", type: "number", required: false, description: "Optional fixed width" },
|
|
871
|
+
{ name: "height", type: "number", required: false, description: "Optional fixed height" },
|
|
872
|
+
{ name: "fillVariableName", type: "string", required: false, description: "Background fill variable name" },
|
|
873
|
+
{ name: "cornerRadiusVariableName", type: "string", required: false, description: "Corner radius variable name" },
|
|
874
|
+
{ name: "cornerRadius", type: "number", required: false, description: "Corner radius literal (prefer variable when available)" }
|
|
875
|
+
]
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
method: "design_create_text",
|
|
879
|
+
category: "create",
|
|
880
|
+
description: "Creates a text node with visible content.",
|
|
881
|
+
params: [
|
|
882
|
+
{ name: "parentId", type: "string", required: false, description: "Parent node name or ID" },
|
|
883
|
+
{ name: "characters", type: "string", required: true, description: 'The visible text label (e.g. "Button", "Submit"). NEVER just "Text".' },
|
|
884
|
+
{ name: "textStyleName", type: "string", required: false, description: "Text style name from the file" },
|
|
885
|
+
{ name: "fillVariableName", type: "string", required: false, description: "Text color variable name" },
|
|
886
|
+
{ name: "name", type: "string", required: false, description: "Node name" }
|
|
887
|
+
]
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
method: "design_create_rectangle",
|
|
891
|
+
category: "create",
|
|
892
|
+
description: "Creates a rectangle. Use ONLY for decorative shapes, dividers, or image placeholders \u2014 NOT for layout containers.",
|
|
893
|
+
params: [
|
|
894
|
+
{ name: "parentId", type: "string", required: false, description: "Parent node name or ID" },
|
|
895
|
+
{ name: "width", type: "number", required: true, description: "Width in pixels" },
|
|
896
|
+
{ name: "height", type: "number", required: true, description: "Height in pixels" },
|
|
897
|
+
{ name: "fillVariableName", type: "string", required: false, description: "Fill variable name" },
|
|
898
|
+
{ name: "cornerRadiusVariableName", type: "string", required: false, description: "Corner radius variable name" },
|
|
899
|
+
{ name: "cornerRadius", type: "number", required: false, description: "Corner radius literal" },
|
|
900
|
+
{ name: "name", type: "string", required: false, description: "Node name" }
|
|
901
|
+
]
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
method: "design_create_ellipse",
|
|
905
|
+
category: "create",
|
|
906
|
+
description: "Creates an ellipse shape.",
|
|
907
|
+
params: [
|
|
908
|
+
{ name: "parentId", type: "string", required: false, description: "Parent node name or ID" },
|
|
909
|
+
{ name: "width", type: "number", required: true, description: "Width in pixels" },
|
|
910
|
+
{ name: "height", type: "number", required: true, description: "Height in pixels" },
|
|
911
|
+
{ name: "fillVariableName", type: "string", required: false, description: "Fill variable name" },
|
|
912
|
+
{ name: "name", type: "string", required: false, description: "Node name" }
|
|
913
|
+
]
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
method: "design_create_line",
|
|
917
|
+
category: "create",
|
|
918
|
+
description: "Creates a line.",
|
|
919
|
+
params: [
|
|
920
|
+
{ name: "parentId", type: "string", required: false, description: "Parent node name or ID" },
|
|
921
|
+
{ name: "length", type: "number", required: false, description: "Line length in pixels" },
|
|
922
|
+
{ name: "strokeVariableName", type: "string", required: false, description: "Stroke color variable name" },
|
|
923
|
+
{ name: "strokeWeight", type: "number", required: false, description: "Stroke weight in pixels" },
|
|
924
|
+
{ name: "name", type: "string", required: false, description: "Node name" }
|
|
925
|
+
]
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
method: "design_create_vector",
|
|
929
|
+
category: "create",
|
|
930
|
+
description: "Creates a vector node with optional SVG path data. Use for custom icons or shapes when no library component exists.",
|
|
931
|
+
params: [
|
|
932
|
+
{ name: "parentId", type: "string", required: false, description: "Parent node name or ID" },
|
|
933
|
+
{ name: "name", type: "string", required: false, description: "Node name" },
|
|
934
|
+
{ name: "vectorPaths", type: "array", required: false, description: 'Array of SVG path objects: [{"windingRule":"NONZERO"|"EVENODD","data":"M0 0 L10 10..."}]' },
|
|
935
|
+
{ name: "width", type: "number", required: false, description: "Vector width in pixels" },
|
|
936
|
+
{ name: "height", type: "number", required: false, description: "Vector height in pixels" },
|
|
937
|
+
{ name: "fillVariableName", type: "string", required: false, description: "Fill color variable name" }
|
|
938
|
+
]
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
method: "design_create_image",
|
|
942
|
+
category: "create",
|
|
943
|
+
description: "Creates an image node from base64 data.",
|
|
944
|
+
params: [
|
|
945
|
+
{ name: "parentId", type: "string", required: false, description: "Parent node name or ID" },
|
|
946
|
+
{ name: "imageBytesBase64", type: "string", required: true, description: "Base64-encoded PNG or JPEG" },
|
|
947
|
+
{ name: "width", type: "number", required: false, description: "Width in pixels" },
|
|
948
|
+
{ name: "height", type: "number", required: false, description: "Height in pixels" },
|
|
949
|
+
{ name: "scaleMode", type: "string", required: false, description: "Image scale mode", enum: ["FILL", "FIT", "CROP", "TILE"] },
|
|
950
|
+
{ name: "name", type: "string", required: false, description: "Node name" }
|
|
951
|
+
]
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
method: "design_create_frame_from_preset",
|
|
955
|
+
category: "create",
|
|
956
|
+
description: "Creates a frame from a device preset.",
|
|
957
|
+
params: [
|
|
958
|
+
{
|
|
959
|
+
name: "preset",
|
|
960
|
+
type: "string",
|
|
961
|
+
required: true,
|
|
962
|
+
description: "Device preset name",
|
|
963
|
+
enum: [
|
|
964
|
+
"iphone_14",
|
|
965
|
+
"iphone_14_plus",
|
|
966
|
+
"iphone_se",
|
|
967
|
+
"android_360",
|
|
968
|
+
"android_412",
|
|
969
|
+
"tablet_768",
|
|
970
|
+
"desktop_1440",
|
|
971
|
+
"desktop_1920"
|
|
972
|
+
]
|
|
973
|
+
},
|
|
974
|
+
{ name: "parentId", type: "string", required: false, description: "Parent node name or ID" }
|
|
975
|
+
]
|
|
976
|
+
},
|
|
977
|
+
// Components
|
|
978
|
+
{
|
|
979
|
+
method: "design_create_component_instance",
|
|
980
|
+
category: "component",
|
|
981
|
+
description: 'Creates an instance of a library or local component. For library components with variants, pass the componentKey (works for both component keys and component set keys). Select a variant with variantKey (exact key) or variantProperties (property map like {"Size":"sm","State":"default"}). Use textOverrides to set text inside the instance (e.g. button label, input placeholder) \u2014 keys are text node names, values are the text. After creation, hardcoded fills/strokes are automatically rebound to the closest matching DS color variable so imported components use semantic colors. Set rebindColors to false to skip.',
|
|
982
|
+
params: [
|
|
983
|
+
{ name: "parentId", type: "string", required: false, description: "Parent node name or ID" },
|
|
984
|
+
{ name: "componentKey", type: "string", required: false, description: "Library component key or component set key" },
|
|
985
|
+
{ name: "variantKey", type: "string", required: false, description: "Specific variant key (from children list) to import a particular variant instead of the default" },
|
|
986
|
+
{ name: "variantProperties", type: "object", required: false, description: 'Variant properties map to select a variant by property values (e.g. {"Size":"sm","State":"default"}). Alternative to variantKey.' },
|
|
987
|
+
{ name: "componentId", type: "string", required: false, description: "Local component ID" },
|
|
988
|
+
{ name: "name", type: "string", required: false, description: "Node name" },
|
|
989
|
+
{ name: "textOverrides", type: "object", required: false, description: 'Text overrides for text nodes inside the instance. Keys are text node names, values are the replacement text (e.g. {"Label":"Submit","Placeholder":"Enter email"}). The first text node can also be targeted with key "_primary".' },
|
|
990
|
+
{ name: "fillVariableId", type: "string", required: false, description: "Color variable ID to apply as the root fill (overrides automatic rebind for the instance root)" },
|
|
991
|
+
{ name: "rebindColors", type: "boolean", required: false, description: "Auto-rebind hardcoded fills/strokes to closest DS color variable (default: true)" }
|
|
992
|
+
]
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
method: "design_create_component",
|
|
996
|
+
category: "component",
|
|
997
|
+
description: "Creates an empty master component.",
|
|
998
|
+
params: [
|
|
999
|
+
{ name: "parentId", type: "string", required: false, description: "Parent node name or ID" },
|
|
1000
|
+
{ name: "name", type: "string", required: false, description: "Component name" },
|
|
1001
|
+
{ name: "width", type: "number", required: false, description: "Width in pixels" },
|
|
1002
|
+
{ name: "height", type: "number", required: false, description: "Height in pixels" }
|
|
1003
|
+
]
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
method: "design_convert_to_component",
|
|
1007
|
+
category: "component",
|
|
1008
|
+
description: "Converts an existing frame to a component.",
|
|
1009
|
+
params: [
|
|
1010
|
+
{ name: "nodeId", type: "string", required: true, description: "Frame node ID to convert" },
|
|
1011
|
+
{ name: "name", type: "string", required: false, description: "Component name" }
|
|
1012
|
+
]
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
method: "design_combine_as_variants",
|
|
1016
|
+
category: "component",
|
|
1017
|
+
description: "Combines 2+ components into a variant set.",
|
|
1018
|
+
params: [
|
|
1019
|
+
{ name: "parentId", type: "string", required: true, description: "Parent node name or ID" },
|
|
1020
|
+
{ name: "nodeIds", type: "array", required: true, description: "Array of 2+ component node IDs" },
|
|
1021
|
+
{ name: "index", type: "number", required: false, description: "Insert index" }
|
|
1022
|
+
]
|
|
1023
|
+
},
|
|
1024
|
+
// Layout
|
|
1025
|
+
{
|
|
1026
|
+
method: "design_set_auto_layout",
|
|
1027
|
+
category: "layout",
|
|
1028
|
+
description: "Sets auto-layout on a frame. Use layoutWrap WRAP for horizontal layouts that wrap children to multiple rows.",
|
|
1029
|
+
params: [
|
|
1030
|
+
{ name: "nodeId", type: "string", required: false, description: "Target node name or ID (defaults to last created frame)" },
|
|
1031
|
+
{ name: "direction", type: "string", required: true, description: "Layout direction", enum: ["VERTICAL", "HORIZONTAL"] },
|
|
1032
|
+
{ name: "layoutWrap", type: "string", required: false, description: "Only for HORIZONTAL: NO_WRAP (default) or WRAP to allow children to wrap to next row", enum: ["NO_WRAP", "WRAP"] },
|
|
1033
|
+
{ name: "counterAxisSpacingVariableName", type: "string", required: false, description: "When layoutWrap WRAP: spacing between wrapped rows (variable name)" },
|
|
1034
|
+
{ name: "counterAxisAlignContent", type: "string", required: false, description: "When layoutWrap WRAP: alignment of wrapped rows", enum: ["AUTO", "SPACE_BETWEEN"] },
|
|
1035
|
+
{ name: "paddingVariableName", type: "string", required: false, description: "Padding variable name (all sides). Overridden by per-side params if provided." },
|
|
1036
|
+
{ name: "paddingTopVariableName", type: "string", required: false, description: "Top padding variable name (overrides paddingVariableName for top)" },
|
|
1037
|
+
{ name: "paddingRightVariableName", type: "string", required: false, description: "Right padding variable name (overrides paddingVariableName for right)" },
|
|
1038
|
+
{ name: "paddingBottomVariableName", type: "string", required: false, description: "Bottom padding variable name (overrides paddingVariableName for bottom)" },
|
|
1039
|
+
{ name: "paddingLeftVariableName", type: "string", required: false, description: "Left padding variable name (overrides paddingVariableName for left)" },
|
|
1040
|
+
{ name: "itemSpacingVariableName", type: "string", required: false, description: "Item spacing variable name" },
|
|
1041
|
+
{ name: "primaryAxisAlignItems", type: "string", required: false, description: "Primary axis alignment", enum: ["MIN", "CENTER", "MAX", "SPACE_BETWEEN"] },
|
|
1042
|
+
{ name: "counterAxisAlignItems", type: "string", required: false, description: "Counter axis alignment", enum: ["MIN", "CENTER", "MAX", "BASELINE"] },
|
|
1043
|
+
{ name: "layoutSizingHorizontal", type: "string", required: false, description: "Horizontal sizing", enum: ["HUG", "FILL"] },
|
|
1044
|
+
{ name: "layoutSizingVertical", type: "string", required: false, description: "Vertical sizing", enum: ["HUG", "FILL"] }
|
|
1045
|
+
]
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
method: "design_set_layout_sizing",
|
|
1049
|
+
category: "layout",
|
|
1050
|
+
description: "Sets sizing behavior and optional per-child alignment override on a node within auto-layout.",
|
|
1051
|
+
params: [
|
|
1052
|
+
{ name: "nodeId", type: "string", required: false, description: "Target node name or ID" },
|
|
1053
|
+
{ name: "layoutSizingHorizontal", type: "string", required: false, description: "Horizontal sizing", enum: ["FIXED", "HUG", "FILL"] },
|
|
1054
|
+
{ name: "layoutSizingVertical", type: "string", required: false, description: "Vertical sizing", enum: ["FIXED", "HUG", "FILL"] },
|
|
1055
|
+
{ name: "layoutAlign", type: "string", required: false, description: "Whether this child stretches on the counter axis. Use STRETCH to fill; INHERIT (default) to not stretch. Counter-axis alignment is set on the parent via counterAxisAlignItems.", enum: ["STRETCH", "INHERIT"] }
|
|
1056
|
+
]
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
method: "design_set_layout_constraints",
|
|
1060
|
+
category: "layout",
|
|
1061
|
+
description: "Sets min/max width/height constraints on a node. Use variable names for token-bound values, or raw numbers as fallback.",
|
|
1062
|
+
params: [
|
|
1063
|
+
{ name: "nodeId", type: "string", required: true, description: "Target node name or ID" },
|
|
1064
|
+
{ name: "minWidthVariableName", type: "string", required: false, description: "Min width variable name" },
|
|
1065
|
+
{ name: "maxWidthVariableName", type: "string", required: false, description: "Max width variable name" },
|
|
1066
|
+
{ name: "minHeightVariableName", type: "string", required: false, description: "Min height variable name" },
|
|
1067
|
+
{ name: "maxHeightVariableName", type: "string", required: false, description: "Max height variable name" },
|
|
1068
|
+
{ name: "minWidth", type: "number", required: false, description: "Min width in pixels (fallback if no variable)" },
|
|
1069
|
+
{ name: "maxWidth", type: "number", required: false, description: "Max width in pixels (fallback if no variable)" },
|
|
1070
|
+
{ name: "minHeight", type: "number", required: false, description: "Min height in pixels (fallback if no variable)" },
|
|
1071
|
+
{ name: "maxHeight", type: "number", required: false, description: "Max height in pixels (fallback if no variable)" }
|
|
1072
|
+
]
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
method: "design_set_resize_constraints",
|
|
1076
|
+
category: "layout",
|
|
1077
|
+
description: "Sets resize constraints (how the node responds to parent resize). Use for absolutely positioned/floating elements so they float correctly on resize. Horizontal: MIN=Left, CENTER=Center, MAX=Right, STRETCH=Left+Right (pin both, width stretches), SCALE=scale with parent. Vertical: MIN=Top, CENTER=Center, MAX=Bottom, STRETCH=Top+Bottom, SCALE=scale.",
|
|
1078
|
+
params: [
|
|
1079
|
+
{ name: "nodeId", type: "string", required: true, description: "Target node name or ID" },
|
|
1080
|
+
{ name: "constraintsHorizontal", type: "string", required: false, description: "Horizontal: MIN (left), CENTER, MAX (right), STRETCH (left+right), SCALE", enum: ["MIN", "MAX", "CENTER", "STRETCH", "SCALE"] },
|
|
1081
|
+
{ name: "constraintsVertical", type: "string", required: false, description: "Vertical: MIN (top), CENTER, MAX (bottom), STRETCH (top+bottom), SCALE", enum: ["MIN", "MAX", "CENTER", "STRETCH", "SCALE"] }
|
|
1082
|
+
]
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
method: "design_set_node_position",
|
|
1086
|
+
category: "layout",
|
|
1087
|
+
description: "Sets x/y position of a node. For floating elements inside an auto-layout frame (badges, FABs, overlays), set absolute: true so the node ignores auto-layout and is positioned by x/y; then use design_set_resize_constraints so it floats correctly on parent resize.",
|
|
1088
|
+
params: [
|
|
1089
|
+
{ name: "nodeId", type: "string", required: true, description: "Target node name or ID" },
|
|
1090
|
+
{ name: "x", type: "number", required: true, description: "X position" },
|
|
1091
|
+
{ name: "y", type: "number", required: true, description: "Y position" },
|
|
1092
|
+
{ name: "absolute", type: "boolean", required: false, description: "If true, set layoutPositioning to ABSOLUTE so the node is taken out of auto-layout flow (use for floating UI elements)" }
|
|
1093
|
+
]
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
method: "design_resize_node",
|
|
1097
|
+
category: "layout",
|
|
1098
|
+
description: "Resizes a node and optionally updates corner radius.",
|
|
1099
|
+
params: [
|
|
1100
|
+
{ name: "nodeId", type: "string", required: false, description: "Target node name or ID" },
|
|
1101
|
+
{ name: "width", type: "number", required: false, description: "New width" },
|
|
1102
|
+
{ name: "height", type: "number", required: false, description: "New height" },
|
|
1103
|
+
{ name: "cornerRadiusVariableName", type: "string", required: false, description: "Corner radius variable name" },
|
|
1104
|
+
{ name: "cornerRadius", type: "number", required: false, description: "Corner radius literal" }
|
|
1105
|
+
]
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
method: "design_append_child",
|
|
1109
|
+
category: "layout",
|
|
1110
|
+
description: "Reparents a node into a new parent.",
|
|
1111
|
+
params: [
|
|
1112
|
+
{ name: "parentId", type: "string", required: true, description: "New parent node name or ID" },
|
|
1113
|
+
{ name: "childId", type: "string", required: true, description: "Child node name or ID to reparent" }
|
|
1114
|
+
]
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
method: "design_group_nodes",
|
|
1118
|
+
category: "layout",
|
|
1119
|
+
description: "Groups multiple nodes together.",
|
|
1120
|
+
params: [
|
|
1121
|
+
{ name: "parentId", type: "string", required: true, description: "Parent node name or ID" },
|
|
1122
|
+
{ name: "nodeIds", type: "array", required: true, description: "Array of node names or IDs to group" }
|
|
1123
|
+
]
|
|
1124
|
+
},
|
|
1125
|
+
// Styling
|
|
1126
|
+
{
|
|
1127
|
+
method: "design_set_effects",
|
|
1128
|
+
category: "styling",
|
|
1129
|
+
description: "Applies an effect style to a node.",
|
|
1130
|
+
params: [
|
|
1131
|
+
{ name: "nodeId", type: "string", required: true, description: "Target node name or ID" },
|
|
1132
|
+
{ name: "effectStyleName", type: "string", required: true, description: "Effect style name from the file" }
|
|
1133
|
+
]
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
method: "design_set_strokes",
|
|
1137
|
+
category: "styling",
|
|
1138
|
+
description: "Sets stroke color, weight, and alignment on a node. Supports uniform weight or per-side weights (top/right/bottom/left).",
|
|
1139
|
+
params: [
|
|
1140
|
+
{ name: "nodeId", type: "string", required: true, description: "Target node name or ID" },
|
|
1141
|
+
{ name: "strokeVariableName", type: "string", required: true, description: "Stroke color variable name" },
|
|
1142
|
+
{ name: "strokeWeight", type: "number", required: false, description: "Uniform stroke weight in pixels (overridden by per-side values)" },
|
|
1143
|
+
{ name: "strokeWeightVariableName", type: "string", required: false, description: "Stroke weight variable name" },
|
|
1144
|
+
{ name: "strokeAlign", type: "string", required: false, description: "Stroke alignment", enum: ["INSIDE", "OUTSIDE", "CENTER"] },
|
|
1145
|
+
{ name: "strokeTopWeight", type: "number", required: false, description: "Top stroke weight (enables individual strokes)" },
|
|
1146
|
+
{ name: "strokeRightWeight", type: "number", required: false, description: "Right stroke weight" },
|
|
1147
|
+
{ name: "strokeBottomWeight", type: "number", required: false, description: "Bottom stroke weight" },
|
|
1148
|
+
{ name: "strokeLeftWeight", type: "number", required: false, description: "Left stroke weight" }
|
|
1149
|
+
]
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
method: "design_set_text_properties",
|
|
1153
|
+
category: "styling",
|
|
1154
|
+
description: "Sets text alignment, truncation, max lines, and auto-sizing mode on a text node.",
|
|
1155
|
+
params: [
|
|
1156
|
+
{ name: "nodeId", type: "string", required: true, description: "Target text node name or ID" },
|
|
1157
|
+
{ name: "textAlignHorizontal", type: "string", required: false, description: "Horizontal alignment", enum: ["LEFT", "CENTER", "RIGHT", "JUSTIFIED"] },
|
|
1158
|
+
{ name: "textAlignVertical", type: "string", required: false, description: "Vertical alignment", enum: ["TOP", "CENTER", "BOTTOM"] },
|
|
1159
|
+
{ name: "textAutoResize", type: "string", required: false, description: "Text auto-sizing mode: WIDTH_AND_HEIGHT (auto width), HEIGHT (fixed width, auto height), NONE (fixed size), TRUNCATE (fixed + clip)", enum: ["NONE", "WIDTH_AND_HEIGHT", "HEIGHT", "TRUNCATE"] },
|
|
1160
|
+
{ name: "textTruncation", type: "string", required: false, description: "Truncation mode", enum: ["DISABLED", "ENDING"] },
|
|
1161
|
+
{ name: "maxLines", type: "number", required: false, description: "Max lines before truncation" }
|
|
1162
|
+
]
|
|
1163
|
+
},
|
|
1164
|
+
// Mode
|
|
1165
|
+
{
|
|
1166
|
+
method: "design_set_frame_variable_mode",
|
|
1167
|
+
category: "mode",
|
|
1168
|
+
description: "Sets light/dark mode override on a frame. The root frame defaults to Light automatically \u2014 child frames inherit from root (AUTO). Only use this to switch the root frame's mode or to override a specific non-root parent frame.",
|
|
1169
|
+
params: [
|
|
1170
|
+
{ name: "frameId", type: "string", required: true, description: "Frame node ID" },
|
|
1171
|
+
{ name: "collectionId", type: "string", required: true, description: "Variable collection ID" },
|
|
1172
|
+
{ name: "modeId", type: "string", required: true, description: "Mode ID" }
|
|
1173
|
+
]
|
|
1174
|
+
},
|
|
1175
|
+
// Finalize
|
|
1176
|
+
{
|
|
1177
|
+
method: "finalize_design_frame",
|
|
1178
|
+
category: "finalize",
|
|
1179
|
+
description: "Renames the root frame and optionally sets its background fill. Should be the last step.",
|
|
1180
|
+
params: [
|
|
1181
|
+
{ name: "frameId", type: "string", required: false, description: "Frame ID (auto-targets root frame if omitted)" },
|
|
1182
|
+
{ name: "name", type: "string", required: true, description: "Final descriptive name for the frame" },
|
|
1183
|
+
{ name: "fillVariableName", type: "string", required: false, description: "Background fill variable name" }
|
|
1184
|
+
]
|
|
1185
|
+
}
|
|
1186
|
+
],
|
|
1187
|
+
// ---- Output rules (AI instructions, not mandatory for all callers) ------
|
|
1188
|
+
outputRules: [
|
|
1189
|
+
"If the user is asking a question and NOT requesting UI on canvas, answer with a helpful reply and output steps: [].",
|
|
1190
|
+
"For multi-element designs, the first step should be design_create_frame with NO parentId and width/height from standard sizes. This creates the root frame.",
|
|
1191
|
+
"The root frame IS the design canvas. Add sections/columns/children directly to it \u2014 do NOT create a wrapper frame inside.",
|
|
1192
|
+
"Last step is ALWAYS finalize_design_frame with a descriptive name and optional fillVariableName for the root background.",
|
|
1193
|
+
'Every design_create_text MUST include actual characters content, NEVER just "Text".',
|
|
1194
|
+
"Every design_create_frame for a visible component MUST include fillVariableName for its background.",
|
|
1195
|
+
"Every frame with children MUST have design_set_auto_layout applied.",
|
|
1196
|
+
"parentId must be a frame or group, NEVER a component instance. When using design_create_component_instance, create a wrapper frame first, add the instance to it, then use the wrapper frame as parentId for text or other children.",
|
|
1197
|
+
"When using design_create_component_instance for UI components (buttons, inputs, cards, alerts), ALWAYS pass textOverrides to set the visible text INSIDE the component. Do NOT create separate design_create_text steps for text that belongs inside a component \u2014 use textOverrides instead.",
|
|
1198
|
+
'Use design_set_layout_sizing with layoutSizingHorizontal:"FILL" on section/column frames (direct children of root) so they stretch to fill root width. Plugin auto-applies FILL width for 2nd-level children.',
|
|
1199
|
+
"Use EXACT variable/style names from the file context \u2014 do not invent names.",
|
|
1200
|
+
"All params MUST be included in each step \u2014 method names alone are useless without their params."
|
|
1201
|
+
],
|
|
1202
|
+
// ---- ID resolution rules -----------------------------------------------
|
|
1203
|
+
idResolutionRules: [
|
|
1204
|
+
"After the first design_create_frame (root frame, no parentId), any create step missing parentId defaults to the root frame.",
|
|
1205
|
+
'Use the "name" param from a previous create step as parentId/nodeId to reference that node. Example: if you create a frame with name:"Card", then parentId:"Card" in a later step targets that frame.',
|
|
1206
|
+
'For design_set_auto_layout/design_resize_node/design_set_layout_sizing: use nodeId:"FrameName" to target a named frame. If nodeId is omitted, it targets the last created frame (or the root).',
|
|
1207
|
+
"finalize_design_frame auto-targets the root frame if frameId is omitted."
|
|
1208
|
+
],
|
|
1209
|
+
// ---- Composition guidelines --------------------------------------------
|
|
1210
|
+
compositionGuidelines: [
|
|
1211
|
+
// --- Sizing hierarchy (enforced by plugin, but model should follow) ---
|
|
1212
|
+
"SIZING HIERARCHY (critical):",
|
|
1213
|
+
" Level 1 \u2014 Root frame: width is ALWAYS FIXED (never FILL/HUG). Height is HUG for pages/screens (>=500px) so content grows, FIXED for small components (<500px like navbars, cards).",
|
|
1214
|
+
" Level 2 \u2014 Sections/columns (direct children of root): ALWAYS layoutSizingHorizontal FILL so they stretch to root width. The plugin auto-applies FILL width, but you should still call design_set_layout_sizing for clarity.",
|
|
1215
|
+
" Level 3 \u2014 Content frames (cards, rows, inputs, buttons): FILL width to stretch within their section, or HUG for inline elements (badges, icons). Use design_set_layout_sizing explicitly.",
|
|
1216
|
+
" Text nodes: FILL width + textAutoResize HEIGHT for body/paragraph text (auto-applied by plugin). HUG for short labels.",
|
|
1217
|
+
// --- General composition ---
|
|
1218
|
+
"The root frame already has VERTICAL auto-layout. Add sections directly as children.",
|
|
1219
|
+
"Use design_create_frame for all containers (sections, columns, cards, inputs, buttons). Rectangles do NOT support children.",
|
|
1220
|
+
"For a 2-column layout: create left + right column frames directly in the root frame, set layout_sizing FILL on columns, change root auto-layout to HORIZONTAL.",
|
|
1221
|
+
"For a page with sections: create section frames directly in the root frame, set layout_sizing FILL on each section.",
|
|
1222
|
+
"For a card: root frame (small size) with card frame (fill + radius) as direct child, children inside card, auto-layout card VERTICAL, finalize.",
|
|
1223
|
+
'When a card or row uses an icon/component: create a wrapper frame (e.g. name:"Card"), add design_create_component_instance as child of that frame, then add text/other children with parentId:"Card" \u2014 never use the instance name as parentId.',
|
|
1224
|
+
"For grouped elements (button = frame + text, input = frame + text): create inner frames with their own auto-layout.",
|
|
1225
|
+
"Apply auto-layout to INNER frames first, then OUTER frames (bottom-up order).",
|
|
1226
|
+
"For text that should wrap (body, paragraph, card/feed content): ensure the text's parent frame has auto-layout, then use design_set_layout_sizing on the text node with layoutSizingHorizontal FILL and design_set_text_properties with textAutoResize HEIGHT so text wraps to the frame width.",
|
|
1227
|
+
"For horizontal layouts that should wrap children to multiple rows (e.g. tag lists, card grids): use design_set_auto_layout with direction HORIZONTAL and layoutWrap WRAP; optionally set counterAxisSpacingVariableName and counterAxisAlignContent (AUTO or SPACE_BETWEEN).",
|
|
1228
|
+
"Floating elements (badge, FAB, overlay, close button): create the node as child of the relevant frame, then design_set_node_position with absolute: true and x, y; then design_set_resize_constraints (e.g. STRETCH+MAX for a bottom bar that stretches width and sticks to bottom)."
|
|
1229
|
+
],
|
|
1230
|
+
// ---- Placeholder sizes -------------------------------------------------
|
|
1231
|
+
placeholderSizes: {
|
|
1232
|
+
desktop: { width: 1440, height: 900, description: '"desktop" / "page" / "landing" / "dashboard" / "screen"' },
|
|
1233
|
+
mobile: { width: 390, height: 844, description: '"mobile" / "phone" / "app"' },
|
|
1234
|
+
tablet: { width: 768, height: 1024, description: '"tablet" / "iPad"' },
|
|
1235
|
+
card: { width: 400, height: 500, description: '"card" / "dialog" / "modal"' },
|
|
1236
|
+
sidebar: { width: 320, height: 600, description: '"sidebar"' },
|
|
1237
|
+
navbar: { width: 1440, height: 64, description: '"navbar" / "header"' },
|
|
1238
|
+
component: { width: 400, height: 200, description: '"button" / "input" / "badge" / small component' }
|
|
1239
|
+
},
|
|
1240
|
+
// ---- Examples ----------------------------------------------------------
|
|
1241
|
+
examples: [
|
|
1242
|
+
{
|
|
1243
|
+
prompt: "Create a primary button",
|
|
1244
|
+
reply: "Primary button with `Action / primary` fill.",
|
|
1245
|
+
steps: [
|
|
1246
|
+
{ method: "design_create_frame", params: { width: 400, height: 200 } },
|
|
1247
|
+
{ method: "design_create_frame", params: { name: "Button", fillVariableName: "Action / primary", cornerRadiusVariableName: "Radius / md" } },
|
|
1248
|
+
{ method: "design_create_text", params: { parentId: "Button", characters: "Button", textStyleName: "bodySm", fillVariableName: "Action / primary-foreground" } },
|
|
1249
|
+
{ method: "design_set_auto_layout", params: { nodeId: "Button", direction: "HORIZONTAL", paddingVariableName: "Spacing / md", itemSpacingVariableName: "Spacing / sm", primaryAxisAlignItems: "CENTER", counterAxisAlignItems: "CENTER", layoutSizingHorizontal: "HUG", layoutSizingVertical: "HUG" } },
|
|
1250
|
+
{ method: "finalize_design_frame", params: { name: "Primary Button" } }
|
|
1251
|
+
]
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
prompt: "Design a login screen",
|
|
1255
|
+
reply: "Desktop login screen with image column and form column.",
|
|
1256
|
+
steps: [
|
|
1257
|
+
{ method: "design_create_frame", params: { width: 1440, height: 900 } },
|
|
1258
|
+
{ method: "design_create_frame", params: { name: "ImageColumn", fillVariableName: "Surface / muted" } },
|
|
1259
|
+
{ method: "design_set_layout_sizing", params: { nodeId: "ImageColumn", layoutSizingHorizontal: "FILL", layoutSizingVertical: "FILL" } },
|
|
1260
|
+
{ method: "design_create_frame", params: { name: "FormColumn", fillVariableName: "Surface / primary" } },
|
|
1261
|
+
{ method: "design_set_layout_sizing", params: { nodeId: "FormColumn", layoutSizingHorizontal: "FILL", layoutSizingVertical: "FILL" } },
|
|
1262
|
+
{ method: "design_create_text", params: { parentId: "FormColumn", characters: "Welcome back", textStyleName: "heading", fillVariableName: "Text / primary", name: "Title" } },
|
|
1263
|
+
{ method: "design_create_frame", params: { parentId: "FormColumn", name: "EmailInput", fillVariableName: "Surface / secondary", cornerRadiusVariableName: "Radius / md" } },
|
|
1264
|
+
{ method: "design_create_text", params: { parentId: "EmailInput", characters: "Email address", textStyleName: "bodySm", fillVariableName: "Text / secondary" } },
|
|
1265
|
+
{ method: "design_set_auto_layout", params: { nodeId: "EmailInput", direction: "HORIZONTAL", paddingVariableName: "Spacing / sm", counterAxisAlignItems: "CENTER", layoutSizingHorizontal: "FILL", layoutSizingVertical: "HUG" } },
|
|
1266
|
+
{ method: "design_create_frame", params: { parentId: "FormColumn", name: "PasswordInput", fillVariableName: "Surface / secondary", cornerRadiusVariableName: "Radius / md" } },
|
|
1267
|
+
{ method: "design_create_text", params: { parentId: "PasswordInput", characters: "Password", textStyleName: "bodySm", fillVariableName: "Text / secondary" } },
|
|
1268
|
+
{ method: "design_set_auto_layout", params: { nodeId: "PasswordInput", direction: "HORIZONTAL", paddingVariableName: "Spacing / sm", counterAxisAlignItems: "CENTER", layoutSizingHorizontal: "FILL", layoutSizingVertical: "HUG" } },
|
|
1269
|
+
{ method: "design_create_frame", params: { parentId: "FormColumn", name: "LoginBtn", fillVariableName: "Action / primary", cornerRadiusVariableName: "Radius / md" } },
|
|
1270
|
+
{ method: "design_create_text", params: { parentId: "LoginBtn", characters: "Sign in", textStyleName: "bodySm", fillVariableName: "Action / primary-foreground" } },
|
|
1271
|
+
{ method: "design_set_auto_layout", params: { nodeId: "LoginBtn", direction: "HORIZONTAL", paddingVariableName: "Spacing / md", primaryAxisAlignItems: "CENTER", counterAxisAlignItems: "CENTER", layoutSizingHorizontal: "FILL", layoutSizingVertical: "HUG" } },
|
|
1272
|
+
{ method: "design_set_auto_layout", params: { nodeId: "FormColumn", direction: "VERTICAL", paddingVariableName: "Spacing / xl", itemSpacingVariableName: "Spacing / md", primaryAxisAlignItems: "CENTER", counterAxisAlignItems: "MIN", layoutSizingHorizontal: "FILL", layoutSizingVertical: "FILL" } },
|
|
1273
|
+
{ method: "design_set_auto_layout", params: { direction: "HORIZONTAL", itemSpacingVariableName: "Spacing / none", primaryAxisAlignItems: "MIN", counterAxisAlignItems: "MIN" } },
|
|
1274
|
+
{ method: "finalize_design_frame", params: { name: "Desktop Login Screen", fillVariableName: "Surface / primary" } }
|
|
1275
|
+
],
|
|
1276
|
+
note: "The last design_set_auto_layout without nodeId targets the root frame (overrides its default VERTICAL layout to HORIZONTAL for the 2-column split)."
|
|
1277
|
+
}
|
|
1278
|
+
]
|
|
1279
|
+
};
|
|
1280
|
+
function getCatalogMethodNames() {
|
|
1281
|
+
return FIGMA_DESIGN_CATALOG.methods.map((m) => m.method);
|
|
1282
|
+
}
|
|
1283
|
+
function getDesignMethodNames() {
|
|
1284
|
+
return FIGMA_DESIGN_CATALOG.methods.filter((m) => m.category !== "query" && m.category !== "sync").map((m) => m.method);
|
|
1285
|
+
}
|
|
1286
|
+
function getQueryMethodNames() {
|
|
1287
|
+
return FIGMA_DESIGN_CATALOG.methods.filter((m) => m.category === "query").map((m) => m.method);
|
|
1288
|
+
}
|
|
1289
|
+
function formatCatalogForMCP(catalog, fileContext) {
|
|
1290
|
+
const designMethods = catalog.methods.filter(
|
|
1291
|
+
(m) => m.category !== "sync"
|
|
1292
|
+
);
|
|
1293
|
+
return {
|
|
1294
|
+
methods: designMethods.map((m) => ({
|
|
1295
|
+
method: m.method,
|
|
1296
|
+
category: m.category,
|
|
1297
|
+
description: m.description,
|
|
1298
|
+
params: m.params
|
|
1299
|
+
})),
|
|
1300
|
+
outputRules: catalog.outputRules,
|
|
1301
|
+
idResolutionRules: catalog.idResolutionRules,
|
|
1302
|
+
compositionGuidelines: catalog.compositionGuidelines,
|
|
1303
|
+
placeholderSizes: catalog.placeholderSizes,
|
|
1304
|
+
examples: catalog.examples,
|
|
1305
|
+
...fileContext ? { fileContext } : {}
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
function normalizeTokenName(name) {
|
|
1309
|
+
return name.toLowerCase().trim().replace(/\s*\/\s*/g, "/").replace(/\s+/g, "/").replace(/\/+/g, "/").replace(/^\//, "").replace(/\/$/, "");
|
|
1310
|
+
}
|
|
1311
|
+
function buildResolvers(fileContext) {
|
|
1312
|
+
const textStyleIdByName = /* @__PURE__ */ new Map();
|
|
1313
|
+
const textStyleIdByNormalized = /* @__PURE__ */ new Map();
|
|
1314
|
+
for (const s of fileContext.textStyles ?? []) {
|
|
1315
|
+
textStyleIdByName.set(s.name, s.id);
|
|
1316
|
+
textStyleIdByNormalized.set(normalizeTokenName(s.name), s.id);
|
|
1317
|
+
}
|
|
1318
|
+
const effectStyleIdByName = /* @__PURE__ */ new Map();
|
|
1319
|
+
const effectStyleIdByNormalized = /* @__PURE__ */ new Map();
|
|
1320
|
+
for (const s of fileContext.effectStyles ?? []) {
|
|
1321
|
+
effectStyleIdByName.set(s.name, s.id);
|
|
1322
|
+
effectStyleIdByNormalized.set(normalizeTokenName(s.name), s.id);
|
|
1323
|
+
}
|
|
1324
|
+
const variableIdByName = /* @__PURE__ */ new Map();
|
|
1325
|
+
const variableIdByNormalized = /* @__PURE__ */ new Map();
|
|
1326
|
+
for (const coll of fileContext.variableCollections ?? []) {
|
|
1327
|
+
for (const v of coll.variables) {
|
|
1328
|
+
variableIdByName.set(v.name, v.id);
|
|
1329
|
+
variableIdByNormalized.set(normalizeTokenName(v.name), v.id);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
for (const coll of fileContext.variableCollectionsLibrary ?? []) {
|
|
1333
|
+
for (const v of coll.variables) {
|
|
1334
|
+
const identifier = v.key ? `libkey:${v.key}` : v.id ?? "";
|
|
1335
|
+
if (!identifier) continue;
|
|
1336
|
+
if (!variableIdByName.has(v.name)) {
|
|
1337
|
+
variableIdByName.set(v.name, identifier);
|
|
1338
|
+
variableIdByNormalized.set(normalizeTokenName(v.name), identifier);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
const firstTextStyleId = (fileContext.textStyles ?? [])[0]?.id;
|
|
1343
|
+
const firstEffectStyleId = (fileContext.effectStyles ?? [])[0]?.id;
|
|
1344
|
+
return {
|
|
1345
|
+
textStyleIdByName,
|
|
1346
|
+
textStyleIdByNormalized,
|
|
1347
|
+
effectStyleIdByName,
|
|
1348
|
+
effectStyleIdByNormalized,
|
|
1349
|
+
variableIdByName,
|
|
1350
|
+
variableIdByNormalized,
|
|
1351
|
+
firstTextStyleId,
|
|
1352
|
+
firstEffectStyleId
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function resolveByNameOrNormalized(name, exact, normalized) {
|
|
1356
|
+
return exact.get(name) ?? normalized.get(normalizeTokenName(name));
|
|
1357
|
+
}
|
|
1358
|
+
function resolveVariableName(name, variableIdByName, variableIdByNormalized) {
|
|
1359
|
+
return variableIdByName.get(name) ?? variableIdByNormalized.get(normalizeTokenName(name)) ?? (name.includes(" / ") ? variableIdByName.get(name.split(" / ").slice(-1)[0]?.trim() ?? "") : void 0) ?? (name.includes("/") ? variableIdByNormalized.get(normalizeTokenName(name.split("/").pop() ?? "")) : void 0);
|
|
1360
|
+
}
|
|
1361
|
+
function resolveStepParams(params, resolvers) {
|
|
1362
|
+
const out = { ...params };
|
|
1363
|
+
const {
|
|
1364
|
+
textStyleIdByName,
|
|
1365
|
+
textStyleIdByNormalized,
|
|
1366
|
+
effectStyleIdByName,
|
|
1367
|
+
effectStyleIdByNormalized,
|
|
1368
|
+
variableIdByName,
|
|
1369
|
+
variableIdByNormalized,
|
|
1370
|
+
firstTextStyleId,
|
|
1371
|
+
firstEffectStyleId
|
|
1372
|
+
} = resolvers;
|
|
1373
|
+
if (typeof out.textStyleName === "string") {
|
|
1374
|
+
const id = resolveByNameOrNormalized(out.textStyleName, textStyleIdByName, textStyleIdByNormalized) ?? firstTextStyleId;
|
|
1375
|
+
if (id) {
|
|
1376
|
+
out.textStyleId = id;
|
|
1377
|
+
delete out.textStyleName;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (typeof out.fillVariableName === "string") {
|
|
1381
|
+
const id = resolveVariableName(out.fillVariableName, variableIdByName, variableIdByNormalized);
|
|
1382
|
+
if (id) {
|
|
1383
|
+
out.fillVariableId = id;
|
|
1384
|
+
delete out.fillVariableName;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
if (typeof out.effectStyleName === "string") {
|
|
1388
|
+
const id = resolveByNameOrNormalized(out.effectStyleName, effectStyleIdByName, effectStyleIdByNormalized) ?? firstEffectStyleId;
|
|
1389
|
+
if (id) {
|
|
1390
|
+
out.effectStyleId = id;
|
|
1391
|
+
delete out.effectStyleName;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (typeof out.strokeVariableName === "string") {
|
|
1395
|
+
const id = resolveVariableName(out.strokeVariableName, variableIdByName, variableIdByNormalized);
|
|
1396
|
+
if (id) {
|
|
1397
|
+
out.strokeVariableId = id;
|
|
1398
|
+
delete out.strokeVariableName;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (typeof out.cornerRadiusVariableName === "string") {
|
|
1402
|
+
const id = resolveVariableName(out.cornerRadiusVariableName, variableIdByName, variableIdByNormalized);
|
|
1403
|
+
if (id) {
|
|
1404
|
+
out.cornerRadiusVariableId = id;
|
|
1405
|
+
delete out.cornerRadiusVariableName;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
if (typeof out.strokeWeightVariableName === "string") {
|
|
1409
|
+
const id = resolveVariableName(out.strokeWeightVariableName, variableIdByName, variableIdByNormalized);
|
|
1410
|
+
if (id) {
|
|
1411
|
+
out.strokeWeightVariableId = id;
|
|
1412
|
+
delete out.strokeWeightVariableName;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (typeof out.paddingVariableName === "string") {
|
|
1416
|
+
const id = resolveVariableName(out.paddingVariableName, variableIdByName, variableIdByNormalized);
|
|
1417
|
+
if (id) {
|
|
1418
|
+
out.paddingVariableId = id;
|
|
1419
|
+
delete out.paddingVariableName;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
if (typeof out.paddingTopVariableName === "string") {
|
|
1423
|
+
const id = resolveVariableName(out.paddingTopVariableName, variableIdByName, variableIdByNormalized);
|
|
1424
|
+
if (id) {
|
|
1425
|
+
out.paddingTopVariableId = id;
|
|
1426
|
+
delete out.paddingTopVariableName;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
if (typeof out.paddingRightVariableName === "string") {
|
|
1430
|
+
const id = resolveVariableName(out.paddingRightVariableName, variableIdByName, variableIdByNormalized);
|
|
1431
|
+
if (id) {
|
|
1432
|
+
out.paddingRightVariableId = id;
|
|
1433
|
+
delete out.paddingRightVariableName;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (typeof out.paddingBottomVariableName === "string") {
|
|
1437
|
+
const id = resolveVariableName(out.paddingBottomVariableName, variableIdByName, variableIdByNormalized);
|
|
1438
|
+
if (id) {
|
|
1439
|
+
out.paddingBottomVariableId = id;
|
|
1440
|
+
delete out.paddingBottomVariableName;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (typeof out.paddingLeftVariableName === "string") {
|
|
1444
|
+
const id = resolveVariableName(out.paddingLeftVariableName, variableIdByName, variableIdByNormalized);
|
|
1445
|
+
if (id) {
|
|
1446
|
+
out.paddingLeftVariableId = id;
|
|
1447
|
+
delete out.paddingLeftVariableName;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (typeof out.itemSpacingVariableName === "string") {
|
|
1451
|
+
const id = resolveVariableName(out.itemSpacingVariableName, variableIdByName, variableIdByNormalized);
|
|
1452
|
+
if (id) {
|
|
1453
|
+
out.itemSpacingVariableId = id;
|
|
1454
|
+
delete out.itemSpacingVariableName;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (typeof out.counterAxisSpacingVariableName === "string") {
|
|
1458
|
+
const id = resolveVariableName(out.counterAxisSpacingVariableName, variableIdByName, variableIdByNormalized);
|
|
1459
|
+
if (id) {
|
|
1460
|
+
out.counterAxisSpacingVariableId = id;
|
|
1461
|
+
delete out.counterAxisSpacingVariableName;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
if (typeof out.minWidthVariableName === "string") {
|
|
1465
|
+
const id = resolveVariableName(out.minWidthVariableName, variableIdByName, variableIdByNormalized);
|
|
1466
|
+
if (id) {
|
|
1467
|
+
out.minWidthVariableId = id;
|
|
1468
|
+
delete out.minWidthVariableName;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (typeof out.maxWidthVariableName === "string") {
|
|
1472
|
+
const id = resolveVariableName(out.maxWidthVariableName, variableIdByName, variableIdByNormalized);
|
|
1473
|
+
if (id) {
|
|
1474
|
+
out.maxWidthVariableId = id;
|
|
1475
|
+
delete out.maxWidthVariableName;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (typeof out.minHeightVariableName === "string") {
|
|
1479
|
+
const id = resolveVariableName(out.minHeightVariableName, variableIdByName, variableIdByNormalized);
|
|
1480
|
+
if (id) {
|
|
1481
|
+
out.minHeightVariableId = id;
|
|
1482
|
+
delete out.minHeightVariableName;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (typeof out.maxHeightVariableName === "string") {
|
|
1486
|
+
const id = resolveVariableName(out.maxHeightVariableName, variableIdByName, variableIdByNormalized);
|
|
1487
|
+
if (id) {
|
|
1488
|
+
out.maxHeightVariableId = id;
|
|
1489
|
+
delete out.maxHeightVariableName;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
return out;
|
|
1493
|
+
}
|
|
1494
|
+
var SCHEMA_VERSION = 1;
|
|
1495
|
+
var FIGMA_PERSONA_ASK = `- When asked who you are or about your role, use the persona defined in the system prompt (Lola, creative assistant for their design system; Ask vs Create help). Steer off-topic conversation back to what they'd like to do in Figma with their design system.
|
|
1496
|
+
- If the user asks you to create, design, or generate UI components or screens on the Figma canvas (e.g. "create a button", "design a login form", "make a card", "add a navbar"), do not attempt to do it. Tell them that to create on the canvas they need to switch to Create mode, then repeat their request there.`;
|
|
1497
|
+
var FIGMA_PERSONA_CREATE = `- When asked who you are or about your role, use the persona defined in the system prompt (Lola, creative assistant for their design system; Ask vs Create help). Steer off-topic conversation back to what they'd like to do in Figma with their design system.
|
|
1498
|
+
- You are the design system expert for Create mode. Expertise: token-based UI (variables, text/effect styles), auto-layout and structure, naming and semantics, consistency with design system tokens, step-by-step Figma bridge commands. Same personality: direct, precise, no fluff; every step and name must earn its place.`;
|
|
1499
|
+
var KNOWLEDGE_ASK_PREREQUISITES = `- You support the design system owner and their team with questions about their brand, design direction, content, and UX. You are brand-agnostic: advise only from their design system's rules (tone of voice, design direction, component and copy guidance). Never impose a different brand.
|
|
1500
|
+
- Use only the "Design system rules" and any tone or direction stated there. If the user has not defined tone or direction, say so and suggest they add it in their design system.`;
|
|
1501
|
+
var KNOWLEDGE_ASK_BEST_PRACTICES = `- Lead with benefit and clarity; avoid corporate fluff and feature-first answers.
|
|
1502
|
+
- For copy or naming: give a concrete before/after when improving something.
|
|
1503
|
+
- For visual or brand questions: tie recommendations to their tokens and rules (e.g. "Given your primary and surface tokens...").
|
|
1504
|
+
- Brainstorm as a partner: offer 2\u20133 clear options when relevant, then narrow with them.
|
|
1505
|
+
- UX and product design: When the question touches flows, layout, or controls, apply UX heuristics (e.g. visibility of system status, match to real world, user control, consistency, error prevention, recognition over recall, flexibility, minimalism, error recovery, help) and interaction design (clear affordances, feedback, mapping, constraints). Recommend in plain language and, when useful, name the principle (e.g. "This improves visibility of system status" or "Recognition over recall: show the option instead of making them remember it").`;
|
|
1506
|
+
var KNOWLEDGE_ASK_DOS_DONTS = `Do:
|
|
1507
|
+
- Ground every recommendation in their design system rules; speak to them as experts; use short, active phrasing; suggest specific wording when discussing copy.
|
|
1508
|
+
- Use UX and interaction design principles to justify design recommendations (e.g. feedback, consistency, clarity of next steps).
|
|
1509
|
+
|
|
1510
|
+
DO NOT:
|
|
1511
|
+
- DO NOT invent tone or direction they have not defined; talk at them; over-explain; use hedging ("we think", "might help") or passive, weak copy.
|
|
1512
|
+
- DO NOT give purely subjective design opinions without tying them to a principle (heuristic, interaction design, or the user's design system rules).`;
|
|
1513
|
+
var KNOWLEDGE_CREATE_PREREQUISITES = `- Must use either linked variables/style library (primary) or local variables/styles (secondary) for binding. Use only token names that appear in the "Available in this Figma file" asset list; do not invent names.
|
|
1514
|
+
- IMPORTANT: If neither linked nor local variables/styles are available, halt the design process and ask the user to run syncToFigma (MCP tool) or /--sync-to-figma (slash prompt) first to set up variables and styles. Do not proceed without them.
|
|
1515
|
+
- Always refer to the user's DS foundation AI guide for design direction and token usage.`;
|
|
1516
|
+
var KNOWLEDGE_CREATE_BEST_PRACTICES = `- When the Design assets catalog lists components (Button, Input, Card, Alert, etc.): you MUST use design_create_component_instance for those elements. Do NOT build buttons, inputs, or cards from design_create_frame + design_create_text. Search the catalog by name and use the componentKey (or componentId for local). Pass textOverrides to set text inside the component (e.g. button labels, input placeholders) and variantProperties to select the correct variant. Only use frame+text when the catalog has no matching component.
|
|
1517
|
+
- For all colors (fills, strokes, text color), use ONLY variable names (fillVariableName, strokeVariableName). NEVER use paint style names for colors \u2014 variables support mode switching (light/dark).
|
|
1518
|
+
- Last step is finalize_design_frame to name and optionally set the background fill on the root frame.
|
|
1519
|
+
- parentId can be omitted on the first child \u2014 it defaults to the root frame. For deeper children, use the "name" from the parent design_create_frame step as parentId.
|
|
1520
|
+
- nodeId in design_set_auto_layout / design_set_layout_sizing / design_set_strokes / design_set_effects is the "name" of the target node (resolved to Figma id at runtime).
|
|
1521
|
+
- After design_create_frame, call design_set_auto_layout, then add children (text/rectangle/ellipse or design_create_component_instance), then design_set_layout_sizing on each child.
|
|
1522
|
+
- Apply cornerRadius via cornerRadiusVariableName; use design_set_strokes for borders; design_set_effects for shadows.`;
|
|
1523
|
+
var KNOWLEDGE_CREATE_DOS_DONTS = `Do:
|
|
1524
|
+
- ALWAYS use SEMANTIC COLOR VARIABLE NAMES (fillVariableName, strokeVariableName) for all colors.
|
|
1525
|
+
- Apply SEMANTIC COLOR VARIABLE NAMES for frames, shapes, and text, change the original color if not compliant.
|
|
1526
|
+
- Give every created frame a clear "name" so later steps can reference it by nodeId.
|
|
1527
|
+
- Output a full sequence of steps for the requested component (e.g. button = frame + auto-layout + text + layout sizing + finalize).
|
|
1528
|
+
- For design_create_text, ALWAYS set "characters" to the real visible label (e.g. "Button", "Submit", "Sign in"). Never use a placeholder like "Text" or leave it empty.
|
|
1529
|
+
- For frames that are buttons, cards, or have a visible background, ALWAYS set fillVariableName to a color variable from the assets list.
|
|
1530
|
+
- For button label text on a colored background, set fillVariableName on design_create_text to a contrasting color variable (e.g. onPrimary).
|
|
1531
|
+
- ALWAYS use design_create_component_instance for icons, logos, illustrations, AND UI components (buttons, inputs, cards, alerts, etc.) when matching components exist in the catalog. Search the catalog by name and use the componentKey (or componentId for local).
|
|
1532
|
+
- For component instances with text (buttons, inputs, cards, alerts, etc.), ALWAYS pass textOverrides to set the visible text INSIDE the component. Use exact text node names from the catalog's textLayers list (e.g. {"Label": "Submit", "Description": "Click here"}). If the catalog shows textLayers for a component, use those exact names as keys. If textLayers are not listed, use get_component_text_layers to discover them, or use {"_primary": "Submit"} to target the first text node. NEVER create separate design_create_text nodes for text that belongs inside a component instance.
|
|
1533
|
+
- For component sets with variants, prefer variantProperties (e.g. {"Size": "sm", "State": "default", "Type": "outline"}) to select the right variant by property values. Use variantKey only when you have the exact key from the catalog children list.
|
|
1534
|
+
- When a card or row contains an icon/component: create a wrapper frame first (design_create_frame with name e.g. "Card"), add design_create_component_instance as child of that frame, then add text/other children with parentId the wrapper frame name \u2014 never use the instance as parentId (Figma cannot append to component instances).
|
|
1535
|
+
- For icons and logo instances, ALWAYS pass fillVariableName with a semantic color (e.g. "Icon / primary", "Text / primary"). You choose the token; the plugin applies it to the glyph only. If you omit fillVariableName, the plugin resolves existing hex fills to the closest variable (prefers semantic) \u2014 so either way icons get semantic color.
|
|
1536
|
+
|
|
1537
|
+
DO NOT:
|
|
1538
|
+
- DO NOT invent variable or style names not in the assets list.
|
|
1539
|
+
- DO NOT use primitive color variables, raw hex, or paint style or raw PX values.
|
|
1540
|
+
- DO NOT set fill or background on the icon frame; fillVariableName on design_create_component_instance applies to the glyph only.
|
|
1541
|
+
- DO NOT use primitive variable names for colors (e.g. "GrayDark / 300", "Gray / 50", "Red / 800") \u2014 use ONLY variables from collections whose name does not include "Primitives" and does not start with "Radix " (e.g. "Text / primary", "Action / primary", "Surface / primary" from semantic collections).
|
|
1542
|
+
- DO NOT output only one or two steps for a component request; include all steps needed to build the UI.
|
|
1543
|
+
- DO NOT use arbitrary frame sizes like 100x100 or 200x200; use standard device sizes (see catalog) or hug content.
|
|
1544
|
+
- DO NOT use design_create_vector for icons when a matching icon component exists in the catalog \u2014 always prefer design_create_component_instance.
|
|
1545
|
+
- DO NOT create separate design_create_text nodes for text that should be inside a component instance (e.g. button labels, input placeholders, alert messages). Use textOverrides on design_create_component_instance instead.
|
|
1546
|
+
- DO NOT build UI components (buttons, inputs, cards, alerts) from scratch with design_create_frame + design_create_text when matching components exist in the catalog. Always use design_create_component_instance with textOverrides and variantProperties.
|
|
1547
|
+
- DO NOT guess text node names for textOverrides. Check the catalog's textLayers data first. If unavailable, call get_component_text_layers before creating the instance to discover the exact text node names.`;
|
|
1548
|
+
var KNOWLEDGE_QUALITY_PROTOCOL = `## Quality Verification
|
|
1549
|
+
|
|
1550
|
+
1. After finalize_design_frame, ALWAYS call get_design_screenshot to visually inspect the result.
|
|
1551
|
+
2. If a source reference was provided (screenshot, image, code snippet, URL), compare the output against it.
|
|
1552
|
+
3. Identify structural mismatches: missing elements, wrong hierarchy, incorrect spacing, misaligned sections.
|
|
1553
|
+
4. Iterate: add or modify steps to fix issues, then screenshot again.
|
|
1554
|
+
5. Maximum 3 iteration rounds before asking the user for guidance on remaining discrepancies.
|
|
1555
|
+
6. Do not declare the design complete until the screenshot confirms it matches the intent.`;
|
|
1556
|
+
var KNOWLEDGE_LAYOUT_DISCIPLINE = `## Auto-Layout & Sizing
|
|
1557
|
+
|
|
1558
|
+
- Auto-layout everything. Every frame with children MUST have design_set_auto_layout applied.
|
|
1559
|
+
- Floating elements (badges, close buttons, FABs, overlays, tooltips): (1) design_set_node_position with absolute: true and x, y; (2) design_set_resize_constraints so they float on parent resize (e.g. STRETCH horizontal + MAX vertical for a bottom bar that sticks to bottom and stretches width).
|
|
1560
|
+
- Use design_set_node_position with absolute: true ONLY for floating elements; it takes the node out of auto-layout so x/y apply.
|
|
1561
|
+
- Root frame width: fixed width, all child frames stretch to fill the root frame width.
|
|
1562
|
+
- Root frame height: Dont fix height, all child frames HUG to fill the root frame height.
|
|
1563
|
+
- Content Width: FILL for content that stretches to parent, HUG for content-sized elements (buttons, tags, chips).
|
|
1564
|
+
- Content Height: Content determines height, but set it to FILL for columns and sidebars that stretch vertically.
|
|
1565
|
+
- Wrapping text: For any body or paragraph text that should wrap within its container (e.g. feed content, card copy, descriptions), (1) set the frame that directly contains the text to layoutSizingHorizontal "FILL" via design_set_layout_sizing so it uses full width, and (2) call design_set_text_properties on that text node with textAutoResize "HEIGHT" so the text wraps to the frame width instead of hugging. Without both, text will hug and leave empty space.
|
|
1566
|
+
- Never leave sizing as default FIXED unless the element has a deliberate fixed dimension.
|
|
1567
|
+
- For icons use icon sizing variables and spacing. Keep icon frames square.
|
|
1568
|
+
- Set elements to hug or fill \u2014 never leave them unconstrained.`;
|
|
1569
|
+
var KNOWLEDGE_MOBILE_DESIGN = `## Designing for Mobile
|
|
1570
|
+
|
|
1571
|
+
### Best Practices
|
|
1572
|
+
1. Avoid fixed width or height. Add padding and set height and width to hug contents or fill width. See spacing and sizing rules!
|
|
1573
|
+
2. Text width should always set to "FILL" to ensure text wraps within frame's width.
|
|
1574
|
+
3. Add system status bar UI and home indicator UI if no components for this is available in the catalog. Use spacing variables to set the padding.
|
|
1575
|
+
4. Use device preset frames for correct dimensions. iPhone 14: 390\xD7844, Android: 412\xD7915 (or 360\xD7800 for compact). Use design_create_frame_from_preset when available.
|
|
1576
|
+
5. All interactive elements must follow sizing and spacing rules. Use sizing / height variables to enforce this consistently.
|
|
1577
|
+
6. Design for the thumb zone: place primary actions in the bottom 60% of the screen. Bottom navigation with 4\u20135 items is the standard pattern.
|
|
1578
|
+
7. Mobile is single-column. Stack sections vertically. Avoid horizontal scrolling for primary content.
|
|
1579
|
+
|
|
1580
|
+
### Figma-Specific Tips
|
|
1581
|
+
1. Use design_create_frame_from_preset (iphone_14, android_412) instead of manual dimensions.
|
|
1582
|
+
2. Set text nodes to textAutoResize: "HEIGHT" for fixed-width, auto-height text (mobile text wraps within screen width).
|
|
1583
|
+
3. Content frames: layoutSizingHorizontal "FILL", layoutSizingVertical "HUG" \u2014 fills width, hugs content height.
|
|
1584
|
+
4. Bottom navigation: horizontal auto-layout frame, FILL width, items spaced with SPACE_BETWEEN, each item is a vertical frame (icon + label).
|
|
1585
|
+
5. Cards in scrollable lists: create a container frame with VERTICAL auto-layout, each card FILL width and HUG height.
|
|
1586
|
+
6. Set the ROOT frame's height to "HUG" content to ensure it stretches to the content height.`;
|
|
1587
|
+
var KNOWLEDGE_WEB_DESIGN = `## Designing for Web
|
|
1588
|
+
|
|
1589
|
+
### Best Practices
|
|
1590
|
+
1. Use 1440\xD7900 desktop frame if size not specified. Design content within a max-width container (see breakpoint tokens) centered in the viewport using counterAxisAlignItems: "CENTER".
|
|
1591
|
+
2. Detect how many sections are needed and create them as children of the root frame.
|
|
1592
|
+
3. Root frame by default is a vertical auto-layout frame with FILL horizontal layout sizing. Create a constrained content frame inside with max-width via design_set_layout_constraints.
|
|
1593
|
+
4. Navigation: horizontal auto-layout, logo on the left, nav links centered or right-aligned, use SPACE_BETWEEN for even distribution.
|
|
1594
|
+
5. All interactive elements must follow sizing and spacing rules. Use sizing / height variables to enforce this consistently.
|
|
1595
|
+
6. Responsive thinking: use min/max width constraints on content containers. Cards and grid items benefit from min-width to prevent collapsing.
|
|
1596
|
+
|
|
1597
|
+
### Figma-Specific Tips
|
|
1598
|
+
1. The root frame IS the viewport. Add full-width sections directly as children (VERTICAL auto-layout by default).
|
|
1599
|
+
2. For multi-column layouts: switch the appropriate parent frame to auto-layout.
|
|
1600
|
+
3. Use layoutSizingHorizontal "FILL" on column frames so they stretch proportionally.
|
|
1601
|
+
4. Apply counterAxisAlignItems "STRETCH" on horizontal parents for equal-height columns.
|
|
1602
|
+
5. Hero sections: VERTICAL auto-layout, CENTER both axes, generous padding tokens (xl or 2xl). Use itemSpacing for vertical rhythm.`;
|
|
1603
|
+
var KNOWLEDGE_TOKEN_APPLICATION = `## Applying Tokens to Designs
|
|
1604
|
+
|
|
1605
|
+
### Best Practices
|
|
1606
|
+
1. ALWAYS use SEMANTIC COLOR VARIABLES (fillVariableName, strokeVariableName) \u2014 never use PRIMITIVE colors, hex, rgb, or paint style names. Variables enable light/dark mode switching.
|
|
1607
|
+
2. If components are using NON-SEMANTIC color fills for any shapes or text, replace them with SEMANTIC color fills.
|
|
1608
|
+
3. For icons and logos: pass fillVariableName on design_create_component_instance. The plugin applies the color to the glyph (vector/shape) only, not the icon frame, and rebinds any existing fill to a semantic variable when possible.
|
|
1609
|
+
4. Use semantic token names over primitive names when both exist: prefer "Surface / primary" over "Neutral / 10", prefer "Action / primary" over "Green / 600".
|
|
1610
|
+
5. Bind spacing via paddingVariableName (uniform or per-side) and itemSpacingVariableName on auto-layout frames. Never hardcode pixel spacing.
|
|
1611
|
+
6. Bind corner radius via cornerRadiusVariableName on frames and rectangles. Reserve raw cornerRadius only when no variable exists.
|
|
1612
|
+
7. Apply text styles via textStyleName for consistent typography. Text styles bundle font family, size, weight, and line height.
|
|
1613
|
+
|
|
1614
|
+
### Figma-Specific Tips
|
|
1615
|
+
1. The asset list ("Available in this Figma file") is the source of truth. Match exact names including slashes and casing.
|
|
1616
|
+
2. Use apply_matching_styles_to_selection for batch token application to existing, unbound designs. It matches by color distance.
|
|
1617
|
+
3. The root frame is automatically set to Light mode. Child frames inherit from root (AUTO). Do NOT call design_set_frame_variable_mode on child frames unless the user explicitly asks for a mode override on a specific section. Only use it on the root frame to switch the entire design's mode, or on a non-root parent frame as an explicit override.
|
|
1618
|
+
4. Per-side padding variables (paddingTopVariableName, etc.) override uniform paddingVariableName \u2014 use for asymmetric layouts like cards with more bottom padding.
|
|
1619
|
+
5. For Icons apply SEMANTIC color to the vector shapes. DO NOT set bg color to the ICON frame background.`;
|
|
1620
|
+
var KNOWLEDGE_IMAGE_TO_FIGMA = `## Image-to-Figma Recreation
|
|
1621
|
+
|
|
1622
|
+
ACTIVATION: Apply this knowledge ONLY when the user has selected a reference image and asks to recreate, reproduce, replicate, or redesign it. A reference image is a selected IMAGE node, RECTANGLE, or FRAME containing a single child. If the selection contains multiple elements, REJECT: reply that you can only recreate from a single reference image and ask the user to flatten or select just one image.
|
|
1623
|
+
|
|
1624
|
+
### Step 1: Determine Device Type from Image Dimensions
|
|
1625
|
+
|
|
1626
|
+
The selection context includes the image dimensions (width, height). Use these to determine the root frame size:
|
|
1627
|
+
- Portrait orientation (height > width \xD7 1.2) \u2192 MOBILE. Use 390\xD7844 root frame. Follow mobile design rules.
|
|
1628
|
+
- Width \u2265 1200 \u2192 DESKTOP. Use 1440\xD7900 root frame. Follow web design rules.
|
|
1629
|
+
- Width 600\u20131199 AND landscape/square \u2192 TABLET. Use 768\xD71024 root frame. Follow web design rules with single-column bias.
|
|
1630
|
+
- If dimensions are unavailable, infer from image content (phone chrome = mobile, browser chrome = desktop).
|
|
1631
|
+
Use the matching standard size from the catalog. Do NOT use the image's raw pixel dimensions as the frame size.
|
|
1632
|
+
|
|
1633
|
+
### Step 2: Systematic Top-Down Decomposition
|
|
1634
|
+
|
|
1635
|
+
Scan the reference image from top to bottom. Identify each distinct horizontal band:
|
|
1636
|
+
1. Status bar / system UI (mobile only)
|
|
1637
|
+
2. Navigation / header
|
|
1638
|
+
3. Content sections (hero, features, lists, grids, forms)
|
|
1639
|
+
4. Footer / bottom navigation (mobile: tab bar)
|
|
1640
|
+
|
|
1641
|
+
Each band becomes a FRAME with auto-layout. Name each frame descriptively (e.g., "Header", "Hero Section", "Destination Cards Grid", "App Download Section").
|
|
1642
|
+
|
|
1643
|
+
### Step 3: Element Identification Within Sections
|
|
1644
|
+
|
|
1645
|
+
For each section, identify every visible element:
|
|
1646
|
+
- **Text**: Read the EXACT text from the image. Use the precise wording \u2014 do not paraphrase or summarize. Match the apparent hierarchy (heading vs body vs caption) to available text styles.
|
|
1647
|
+
- **Buttons / CTAs**: Frame + text with action color variables. Match the visual style (filled = primary, outlined = secondary).
|
|
1648
|
+
- **Input fields**: Frame + placeholder text with surface/muted fills.
|
|
1649
|
+
- **Cards**: Frame with fill + corner radius, containing image placeholder + text + metadata.
|
|
1650
|
+
- **Content images** (photos, illustrations, avatars): Create a RECTANGLE placeholder with a muted/secondary fill. Set dimensions proportional to what appears in the reference. Name descriptively (e.g., "New York Photo", "Hero Illustration", "User Avatar").
|
|
1651
|
+
- **Icons**: Use design_create_component_instance if a matching icon exists in the catalog. Otherwise use a small rectangle placeholder.
|
|
1652
|
+
- **Dividers / separators**: design_create_line or a thin frame with border fill.
|
|
1653
|
+
- **Badges / chips / tags**: Small frames with HUG sizing, rounded corners, and appropriate fill.
|
|
1654
|
+
|
|
1655
|
+
### Step 4: Proportional Sizing and Spacing
|
|
1656
|
+
|
|
1657
|
+
Estimate proportions from the reference image relative to the root frame, not pixel-exact:
|
|
1658
|
+
- If a hero section takes roughly 40% of the viewport height, size accordingly.
|
|
1659
|
+
- If cards are arranged in a 2\xD74 or 3\xD72 grid, use horizontal auto-layout with FILL children.
|
|
1660
|
+
- Match the visual density: tight spacing vs generous whitespace. Use spacing variables (sm for tight, md for normal, lg for generous, xl for spacious).
|
|
1661
|
+
- For text sizing: large headings \u2192 largest available text style, body \u2192 mid-range style, captions \u2192 smallest style.
|
|
1662
|
+
|
|
1663
|
+
### Step 5: Color Mapping Strategy
|
|
1664
|
+
|
|
1665
|
+
Map visual colors from the image to the CLOSEST SEMANTIC variable \u2014 do not try to match exact hex values:
|
|
1666
|
+
- Dark backgrounds \u2192 use the darkest available surface/background variable
|
|
1667
|
+
- Light backgrounds \u2192 Surface / primary or Surface / muted
|
|
1668
|
+
- Brand-colored elements (buttons, highlights) \u2192 Action / primary or brand variables
|
|
1669
|
+
- Text on dark \u2192 light text variable; text on light \u2192 dark text variable
|
|
1670
|
+
- Subtle borders \u2192 Border / primary or Border / secondary
|
|
1671
|
+
- Muted/disabled elements \u2192 Text / muted or Surface / muted
|
|
1672
|
+
|
|
1673
|
+
### Content Images and Photos
|
|
1674
|
+
|
|
1675
|
+
Photos, illustrations, screenshots, and complex graphics CANNOT be recreated. For each:
|
|
1676
|
+
- Look for image library or image components.
|
|
1677
|
+
- If none, create a rectangle placeholder with muted or any semantic surface / background color fill. Do not use primitive colors or hex values.
|
|
1678
|
+
- Size proportionally to the reference.
|
|
1679
|
+
- Name descriptively: "Hero Banner Image", "Product Photo", "Map Preview".
|
|
1680
|
+
- DO NOT attempt to describe or recreate photo content as vector shapes`;
|
|
1681
|
+
var KNOWLEDGE_COMPONENTS_VARIANTS = `## Creating Components & Variants
|
|
1682
|
+
|
|
1683
|
+
### Best Practices
|
|
1684
|
+
1. Build the base component fully first: frame + auto-layout + children + token bindings + sizing. Finalize structure before converting.
|
|
1685
|
+
2. If components are using NON-SEMANTIC color fills for any shapes or text, replace them with SEMANTIC color fills.
|
|
1686
|
+
3. Avoid fixed width or height. Add padding and set height and width to hug content. See spacing and sizing rules!
|
|
1687
|
+
4. Name with slash convention for organization: "Button/Primary", "Input/Default", "Card/Outlined". Figma groups these in the assets panel.
|
|
1688
|
+
5. Every variant must be a unique combination of properties (e.g., Size=sm + State=default vs Size=sm + State=hover). Do not duplicate property combinations.
|
|
1689
|
+
6. Use variable binding on all variants so they inherit mode switching (light/dark) automatically. Do NOT set explicit variable mode on variant frames \u2014 they inherit from the root frame.
|
|
1690
|
+
7. NEVER use primitive colors or hardcoded colors for UI components.
|
|
1691
|
+
|
|
1692
|
+
### Figma-Specific Tips
|
|
1693
|
+
1. Use design_create_component for the master, build its internals (auto-layout, children, tokens), then create variant frames and use design_combine_as_variants.
|
|
1694
|
+
2. Alternative: build all variant frames first, use design_convert_to_component on each, then design_combine_as_variants to merge into a variant set.
|
|
1695
|
+
3. Set auto-layout and layout sizing BEFORE converting to component. Changing structure after conversion can break variant overrides.
|
|
1696
|
+
4. Use list_local_components before creating to avoid duplicating existing components in the file.
|
|
1697
|
+
5. Variant property naming must be consistent across all variants in the set (e.g., always "Size", never mixing "Size" and "size").`;
|
|
1698
|
+
var FIGMA_SKILL_EXTRAS_ASK = {
|
|
1699
|
+
schema_version: SCHEMA_VERSION,
|
|
1700
|
+
prerequisites: `${FIGMA_PERSONA_ASK}
|
|
1701
|
+
${KNOWLEDGE_ASK_PREREQUISITES}`,
|
|
1702
|
+
best_practices: KNOWLEDGE_ASK_BEST_PRACTICES,
|
|
1703
|
+
dos_donts: KNOWLEDGE_ASK_DOS_DONTS
|
|
1704
|
+
};
|
|
1705
|
+
var FIGMA_SKILL_EXTRAS_CREATE = {
|
|
1706
|
+
schema_version: SCHEMA_VERSION,
|
|
1707
|
+
prerequisites: `${FIGMA_PERSONA_CREATE}
|
|
1708
|
+
${KNOWLEDGE_CREATE_PREREQUISITES}`,
|
|
1709
|
+
best_practices: [
|
|
1710
|
+
KNOWLEDGE_CREATE_BEST_PRACTICES,
|
|
1711
|
+
KNOWLEDGE_LAYOUT_DISCIPLINE,
|
|
1712
|
+
KNOWLEDGE_QUALITY_PROTOCOL,
|
|
1713
|
+
KNOWLEDGE_MOBILE_DESIGN,
|
|
1714
|
+
KNOWLEDGE_WEB_DESIGN,
|
|
1715
|
+
KNOWLEDGE_TOKEN_APPLICATION,
|
|
1716
|
+
KNOWLEDGE_COMPONENTS_VARIANTS,
|
|
1717
|
+
KNOWLEDGE_IMAGE_TO_FIGMA
|
|
1718
|
+
].join("\n\n"),
|
|
1719
|
+
dos_donts: KNOWLEDGE_CREATE_DOS_DONTS
|
|
1720
|
+
};
|
|
1721
|
+
var FIGMA_DESIGN_SKILL_MD = `# Figma Design Skill
|
|
1722
|
+
|
|
1723
|
+
Create high-quality, token-bound designs in Figma from any reference: screenshots, images, code snippets, or URLs. Every element uses the user's design system variables and styles.
|
|
1724
|
+
|
|
1725
|
+
## Prerequisites
|
|
1726
|
+
|
|
1727
|
+
${KNOWLEDGE_CREATE_PREREQUISITES}
|
|
1728
|
+
|
|
1729
|
+
## Core Workflow
|
|
1730
|
+
|
|
1731
|
+
${KNOWLEDGE_CREATE_BEST_PRACTICES}
|
|
1732
|
+
|
|
1733
|
+
${KNOWLEDGE_LAYOUT_DISCIPLINE}
|
|
1734
|
+
|
|
1735
|
+
${KNOWLEDGE_QUALITY_PROTOCOL}
|
|
1736
|
+
|
|
1737
|
+
${KNOWLEDGE_MOBILE_DESIGN}
|
|
1738
|
+
|
|
1739
|
+
${KNOWLEDGE_WEB_DESIGN}
|
|
1740
|
+
|
|
1741
|
+
${KNOWLEDGE_TOKEN_APPLICATION}
|
|
1742
|
+
|
|
1743
|
+
${KNOWLEDGE_COMPONENTS_VARIANTS}
|
|
1744
|
+
|
|
1745
|
+
${KNOWLEDGE_IMAGE_TO_FIGMA}
|
|
1746
|
+
|
|
1747
|
+
## Do's and Don'ts
|
|
1748
|
+
|
|
1749
|
+
${KNOWLEDGE_CREATE_DOS_DONTS}
|
|
1750
|
+
`;
|
|
1751
|
+
|
|
1752
|
+
// src/figma-bridge-protocol.ts
|
|
1753
|
+
var BRIDGE_METHODS = getCatalogMethodNames();
|
|
1754
|
+
function isBridgeRequest(msg) {
|
|
1755
|
+
if (!msg || typeof msg !== "object") return false;
|
|
1756
|
+
const m = msg;
|
|
1757
|
+
return typeof m.id === "string" && m.id.length > 0 && typeof m.method === "string" && m.method.length > 0;
|
|
1758
|
+
}
|
|
1759
|
+
function normalizeBridgeMethod(method) {
|
|
1760
|
+
if (typeof method !== "string" || !method) return method;
|
|
1761
|
+
return method.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
1762
|
+
}
|
|
1763
|
+
function isAllowedMethod(method) {
|
|
1764
|
+
const normalized = normalizeBridgeMethod(method);
|
|
1765
|
+
return BRIDGE_METHODS.includes(normalized);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
export {
|
|
1769
|
+
buildFigmaPayloadsFromDS,
|
|
1770
|
+
FIGMA_DESIGN_CATALOG,
|
|
1771
|
+
getDesignMethodNames,
|
|
1772
|
+
getQueryMethodNames,
|
|
1773
|
+
formatCatalogForMCP,
|
|
1774
|
+
buildResolvers,
|
|
1775
|
+
resolveStepParams,
|
|
1776
|
+
FIGMA_DESIGN_SKILL_MD,
|
|
1777
|
+
BRIDGE_METHODS,
|
|
1778
|
+
isBridgeRequest,
|
|
1779
|
+
normalizeBridgeMethod,
|
|
1780
|
+
isAllowedMethod
|
|
1781
|
+
};
|
|
1782
|
+
//# sourceMappingURL=chunk-426RNS3G.js.map
|