figma-rust-mcp 3.0.4 → 3.0.5
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/package.json +1 -1
- package/plugin/code.js +563 -72
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-rust-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.5",
|
|
4
4
|
"description": "Claude Code to Figma — Bidirectional MCP bridge. AI draws UI on Figma canvas, reads designs back. Works with Claude, Cursor, VS Code, Windsurf, Antigravity. No API key needed.",
|
|
5
5
|
"mcpName": "io.github.BuiHung1612/figma-mcp",
|
|
6
6
|
"type": "module",
|
package/plugin/code.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// figma-mcp — Figma Plugin main thread v1.2.1
|
|
2
2
|
// Handles both WRITE (draw UI) and READ (extract design) operations.
|
|
3
3
|
// AUTO-GENERATED by scripts/build-plugin.js — edit plugin-src/*.js instead
|
|
4
|
-
// Generated: 2026-08-
|
|
4
|
+
// Generated: 2026-08-30T10:25:26.211Z
|
|
5
5
|
|
|
6
6
|
// ── plugin-src/utils.js ──
|
|
7
7
|
// ─── UTILS ────────────────────────────────────────────────────────────────────
|
|
@@ -347,53 +347,129 @@ function cacheNode(node) {
|
|
|
347
347
|
return node;
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
function normalizeNodeId(id) {
|
|
351
|
+
if (!id || typeof id !== "string") return id;
|
|
352
|
+
var clean = id.trim().replace(/^['"]|['"]$/g, "");
|
|
353
|
+
try { clean = decodeURIComponent(clean); } catch(e) {}
|
|
354
|
+
// Convert URL hyphenated IDs (e.g. 2715-40862) to colon notation (2715:40862)
|
|
355
|
+
clean = clean.replace(/(\d+)-(\d+)/g, "$1:$2");
|
|
356
|
+
return clean;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
var allPagesLoaded = false;
|
|
360
|
+
async function ensureAllPagesLoaded() {
|
|
361
|
+
if (allPagesLoaded) return;
|
|
362
|
+
if (typeof figma.loadAllPagesAsync === "function") {
|
|
363
|
+
try {
|
|
364
|
+
await figma.loadAllPagesAsync();
|
|
365
|
+
allPagesLoaded = true;
|
|
366
|
+
} catch(e) {}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function getNodeNotFoundContext(id, name) {
|
|
371
|
+
var currentPageName = figma.currentPage ? figma.currentPage.name : "unknown";
|
|
372
|
+
var availablePages = figma.root && figma.root.children ? figma.root.children.map(function(p) { return p.name; }).join(", ") : "none";
|
|
373
|
+
var selInfo = figma.currentPage && figma.currentPage.selection && figma.currentPage.selection.length > 0
|
|
374
|
+
? figma.currentPage.selection.map(function(s) { return s.id + " (" + s.name + ")"; }).join(", ")
|
|
375
|
+
: "none";
|
|
376
|
+
var target = id || name || "no id/name given";
|
|
377
|
+
return "Node not found: " + target + ". [Active Page: \"" + currentPageName + "\", Available Pages: [" + availablePages + "], Current Selection: [" + selInfo + "]]. Use figma_read get_page_nodes to list nodes or set_page to switch pages.";
|
|
378
|
+
}
|
|
379
|
+
|
|
350
380
|
function findNodeById(id) {
|
|
351
381
|
if (!id) return null;
|
|
352
|
-
|
|
353
|
-
if (figma.
|
|
354
|
-
if (
|
|
355
|
-
|
|
382
|
+
var cleanId = normalizeNodeId(id);
|
|
383
|
+
if (figma.currentPage.id === cleanId || figma.currentPage.id === id) return figma.currentPage;
|
|
384
|
+
if (figma.root.id === cleanId || figma.root.id === id) return figma.root;
|
|
385
|
+
if (nodeCache.has(cleanId)) {
|
|
386
|
+
var cached = nodeCache.get(cleanId);
|
|
356
387
|
if (!cached.removed) return cached;
|
|
388
|
+
nodeCache.delete(cleanId);
|
|
389
|
+
}
|
|
390
|
+
if (nodeCache.has(id)) {
|
|
391
|
+
var cached2 = nodeCache.get(id);
|
|
392
|
+
if (!cached2.removed) return cached2;
|
|
357
393
|
nodeCache.delete(id);
|
|
358
394
|
}
|
|
359
395
|
try {
|
|
360
396
|
if (typeof figma.getNodeById === "function") {
|
|
361
|
-
var node = figma.getNodeById(id);
|
|
397
|
+
var node = figma.getNodeById(cleanId) || (cleanId !== id ? figma.getNodeById(id) : null);
|
|
362
398
|
if (node && !node.removed) return cacheNode(node);
|
|
363
399
|
}
|
|
364
400
|
} catch(e) {}
|
|
365
401
|
// Check selection
|
|
366
402
|
for (var i = 0; i < figma.currentPage.selection.length; i++) {
|
|
367
|
-
|
|
403
|
+
var sel = figma.currentPage.selection[i];
|
|
404
|
+
if (sel.id === cleanId || sel.id === id) return cacheNode(sel);
|
|
368
405
|
}
|
|
369
406
|
return null;
|
|
370
407
|
}
|
|
371
408
|
|
|
372
409
|
async function findNodeByIdAsync(id) {
|
|
373
410
|
if (!id) return null;
|
|
374
|
-
|
|
375
|
-
if (figma.
|
|
376
|
-
if (
|
|
377
|
-
|
|
411
|
+
var cleanId = normalizeNodeId(id);
|
|
412
|
+
if (figma.currentPage.id === cleanId || figma.currentPage.id === id) return figma.currentPage;
|
|
413
|
+
if (figma.root.id === cleanId || figma.root.id === id) return figma.root;
|
|
414
|
+
if (nodeCache.has(cleanId)) {
|
|
415
|
+
var cached = nodeCache.get(cleanId);
|
|
378
416
|
if (!cached.removed) return cached;
|
|
417
|
+
nodeCache.delete(cleanId);
|
|
418
|
+
}
|
|
419
|
+
if (nodeCache.has(id)) {
|
|
420
|
+
var cached2 = nodeCache.get(id);
|
|
421
|
+
if (!cached2.removed) return cached2;
|
|
379
422
|
nodeCache.delete(id);
|
|
380
423
|
}
|
|
381
|
-
|
|
424
|
+
|
|
425
|
+
// 1. Try synchronous lookup first (0ms)
|
|
382
426
|
try {
|
|
383
427
|
if (typeof figma.getNodeById === "function") {
|
|
384
|
-
var syncNode = figma.getNodeById(id);
|
|
428
|
+
var syncNode = figma.getNodeById(cleanId) || (cleanId !== id ? figma.getNodeById(id) : null);
|
|
385
429
|
if (syncNode && !syncNode.removed) return cacheNode(syncNode);
|
|
386
430
|
}
|
|
387
431
|
} catch(e) {}
|
|
388
|
-
|
|
432
|
+
|
|
433
|
+
// 2. Try async lookup
|
|
389
434
|
try {
|
|
390
|
-
var node = await figma.getNodeByIdAsync(
|
|
435
|
+
var node = await figma.getNodeByIdAsync(cleanId);
|
|
436
|
+
if (!node && cleanId !== id) node = await figma.getNodeByIdAsync(id);
|
|
391
437
|
if (node && !node.removed) return cacheNode(node);
|
|
392
438
|
} catch(e) {}
|
|
439
|
+
|
|
440
|
+
// 3. Check selection
|
|
441
|
+
for (var i = 0; i < figma.currentPage.selection.length; i++) {
|
|
442
|
+
var s = figma.currentPage.selection[i];
|
|
443
|
+
if (s.id === cleanId || s.id === id) return cacheNode(s);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 4. Fallback: If not found and pages are lazily loaded, load all pages and retry
|
|
447
|
+
if (!allPagesLoaded && typeof figma.loadAllPagesAsync === "function") {
|
|
448
|
+
await ensureAllPagesLoaded();
|
|
449
|
+
try {
|
|
450
|
+
var retryNode = await figma.getNodeByIdAsync(cleanId);
|
|
451
|
+
if (!retryNode && cleanId !== id) retryNode = await figma.getNodeByIdAsync(id);
|
|
452
|
+
if (retryNode && !retryNode.removed) return cacheNode(retryNode);
|
|
453
|
+
} catch(e) {}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 5. Fallback for instance sub-layer IDs (e.g. "I2715:40862;123:456")
|
|
457
|
+
if (cleanId.indexOf(";") !== -1) {
|
|
458
|
+
var parts = cleanId.split(";");
|
|
459
|
+
for (var p = 0; p < parts.length; p++) {
|
|
460
|
+
var partId = parts[p].replace(/^I/, "");
|
|
461
|
+
try {
|
|
462
|
+
var subNode = await figma.getNodeByIdAsync(partId);
|
|
463
|
+
if (subNode && !subNode.removed) return cacheNode(subNode);
|
|
464
|
+
} catch(e) {}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
393
468
|
return null;
|
|
394
469
|
}
|
|
395
470
|
|
|
396
471
|
function findNodeByName(name) {
|
|
472
|
+
if (!name) return null;
|
|
397
473
|
if (figma.currentPage.name === name) return figma.currentPage;
|
|
398
474
|
// Search in selection first, then shallow children, then findOne as fallback
|
|
399
475
|
for (var i = 0; i < figma.currentPage.selection.length; i++) {
|
|
@@ -402,20 +478,34 @@ function findNodeByName(name) {
|
|
|
402
478
|
for (var j = 0; j < figma.currentPage.children.length; j++) {
|
|
403
479
|
if (figma.currentPage.children[j].name === name) return cacheNode(figma.currentPage.children[j]);
|
|
404
480
|
}
|
|
405
|
-
var found = figma.currentPage.findOne(n
|
|
406
|
-
if (found) cacheNode(found);
|
|
407
|
-
|
|
481
|
+
var found = figma.currentPage.findOne(function(n) { return n.name === name; });
|
|
482
|
+
if (found) return cacheNode(found);
|
|
483
|
+
|
|
484
|
+
// Cross-page fallback
|
|
485
|
+
if (figma.root && figma.root.children) {
|
|
486
|
+
for (var p = 0; p < figma.root.children.length; p++) {
|
|
487
|
+
var page = figma.root.children[p];
|
|
488
|
+
if (page === figma.currentPage) continue;
|
|
489
|
+
if (page.name === name) return page;
|
|
490
|
+
var pageFound = page.findOne ? page.findOne(function(n) { return n.name === name; }) : null;
|
|
491
|
+
if (pageFound) return cacheNode(pageFound);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return null;
|
|
408
496
|
}
|
|
409
497
|
|
|
410
498
|
async function resolveNode(params) {
|
|
411
|
-
|
|
412
|
-
var
|
|
499
|
+
if (!params) return null;
|
|
500
|
+
var id = params.id || params.nodeId || params.node_id || params.targetId || params.target_id;
|
|
501
|
+
var name = params.name || params.nodeName || params.node_name;
|
|
413
502
|
var node = null;
|
|
414
503
|
if (id) node = await findNodeByIdAsync(id);
|
|
415
504
|
if (!node && name) node = findNodeByName(name);
|
|
416
505
|
return node;
|
|
417
506
|
}
|
|
418
507
|
|
|
508
|
+
|
|
419
509
|
function nodeToInfo(node) {
|
|
420
510
|
if (!node) return null;
|
|
421
511
|
const info = {
|
|
@@ -438,6 +528,71 @@ function yieldToUI(delayMs) {
|
|
|
438
528
|
});
|
|
439
529
|
}
|
|
440
530
|
|
|
531
|
+
// Flexible operation handler resolver with camelCase/snake_case and alias support
|
|
532
|
+
function resolveOperationHandler(operation) {
|
|
533
|
+
if (!operation || typeof operation !== "string") return null;
|
|
534
|
+
if (typeof handlers !== "object" || !handlers) return null;
|
|
535
|
+
if (handlers[operation]) return handlers[operation];
|
|
536
|
+
|
|
537
|
+
// 1. Convert snake_case -> camelCase (e.g. create_variable -> createVariable, list_pages -> listPages)
|
|
538
|
+
var camel = operation.replace(/_([a-z0-9])/gi, function(_, c) { return c.toUpperCase(); });
|
|
539
|
+
if (handlers[camel]) return handlers[camel];
|
|
540
|
+
|
|
541
|
+
// 2. Convert camelCase -> snake_case (e.g. getPageNodes -> get_page_nodes, getNodeDetail -> get_node_detail)
|
|
542
|
+
var snake = operation.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
543
|
+
if (handlers[snake]) return handlers[snake];
|
|
544
|
+
|
|
545
|
+
// 3. Common semantic synonyms & aliases
|
|
546
|
+
var normalized = operation.toLowerCase().replace(/[-_\s]+/g, "");
|
|
547
|
+
var aliasMap = {
|
|
548
|
+
"getnode": handlers.get_node_detail,
|
|
549
|
+
"getnodedetail": handlers.get_node_detail,
|
|
550
|
+
"getnodeinfo": handlers.get_node_detail,
|
|
551
|
+
"nodedetail": handlers.get_node_detail,
|
|
552
|
+
"nodeinfo": handlers.get_node_detail,
|
|
553
|
+
"inspectnode": handlers.get_design_context,
|
|
554
|
+
"inspect": handlers.get_design_context,
|
|
555
|
+
"getdesigncontext": handlers.get_design_context,
|
|
556
|
+
"designcontext": handlers.get_design_context,
|
|
557
|
+
"getdesign": handlers.get_design,
|
|
558
|
+
"getselection": handlers.get_selection,
|
|
559
|
+
"selection": handlers.get_selection,
|
|
560
|
+
"getpagenodes": handlers.get_page_nodes,
|
|
561
|
+
"pagenodes": handlers.get_page_nodes,
|
|
562
|
+
"getstyles": handlers.get_styles,
|
|
563
|
+
"styles": handlers.get_styles,
|
|
564
|
+
"getvariables": handlers.get_variables,
|
|
565
|
+
"gettokens": handlers.get_variables,
|
|
566
|
+
"tokens": handlers.get_variables,
|
|
567
|
+
"variables": handlers.get_variables,
|
|
568
|
+
"getvariabletokens": handlers.get_variables,
|
|
569
|
+
"getlocalcomponents": handlers.get_local_components,
|
|
570
|
+
"localcomponents": handlers.get_local_components,
|
|
571
|
+
"listcomponents": handlers.listComponents,
|
|
572
|
+
"components": handlers.get_local_components,
|
|
573
|
+
"getcomponentmap": handlers.get_component_map,
|
|
574
|
+
"getunmappedcomponents": handlers.get_unmapped_components,
|
|
575
|
+
"scandesign": handlers.scan_design,
|
|
576
|
+
"searchnodes": handlers.search_nodes,
|
|
577
|
+
"getviewport": handlers.get_viewport,
|
|
578
|
+
"setviewport": handlers.set_viewport,
|
|
579
|
+
"exportsvg": handlers.export_svg,
|
|
580
|
+
"exportimage": handlers.export_image,
|
|
581
|
+
"exportassets": handlers.export_assets,
|
|
582
|
+
"listpages": handlers.listPages,
|
|
583
|
+
"setpage": handlers.setPage,
|
|
584
|
+
"createpage": handlers.createPage,
|
|
585
|
+
"loadallpages": handlers.loadAllPagesAsync,
|
|
586
|
+
"loadallpagesasync": handlers.loadAllPagesAsync,
|
|
587
|
+
"update": handlers.modify,
|
|
588
|
+
"remove": handlers["delete"]
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
if (aliasMap[normalized]) return aliasMap[normalized];
|
|
592
|
+
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
|
|
441
596
|
|
|
442
597
|
// ── plugin-src/svg-path-helpers.js ──
|
|
443
598
|
// ─── SVG PATH NORMALIZATION ───────────────────────────────────────────────────
|
|
@@ -868,6 +1023,150 @@ function resolveTextStyle(node, opts) {
|
|
|
868
1023
|
return out;
|
|
869
1024
|
}
|
|
870
1025
|
|
|
1026
|
+
// ── Design Token / Variable Resolver Map ──
|
|
1027
|
+
var cachedVariableResolverMap = null;
|
|
1028
|
+
var cachedVariableResolverMapTime = 0;
|
|
1029
|
+
|
|
1030
|
+
async function buildVariableResolverMapAsync(forceRefresh) {
|
|
1031
|
+
var now = Date.now();
|
|
1032
|
+
if (!forceRefresh && cachedVariableResolverMap && (now - cachedVariableResolverMapTime < 15000)) {
|
|
1033
|
+
return cachedVariableResolverMap;
|
|
1034
|
+
}
|
|
1035
|
+
var map = { byId: {}, byName: {} };
|
|
1036
|
+
try {
|
|
1037
|
+
if (typeof figma.variables !== "undefined" && typeof figma.variables.getLocalVariablesAsync === "function") {
|
|
1038
|
+
var vars = await figma.variables.getLocalVariablesAsync();
|
|
1039
|
+
var rawMap = {};
|
|
1040
|
+
for (var i = 0; i < vars.length; i++) {
|
|
1041
|
+
var v = vars[i];
|
|
1042
|
+
if (v && v.id) rawMap[v.id] = v;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function resolveVarValue(v, depth) {
|
|
1046
|
+
if (!v || depth > 6) return null;
|
|
1047
|
+
var defaultVal = null;
|
|
1048
|
+
if (v.valuesByMode) {
|
|
1049
|
+
var modeKeys = Object.keys(v.valuesByMode);
|
|
1050
|
+
if (modeKeys.length > 0) defaultVal = v.valuesByMode[modeKeys[0]];
|
|
1051
|
+
}
|
|
1052
|
+
if (defaultVal && typeof defaultVal === "object" && defaultVal.type === "VARIABLE_ALIAS" && defaultVal.id) {
|
|
1053
|
+
var targetVar = rawMap[defaultVal.id];
|
|
1054
|
+
if (targetVar) {
|
|
1055
|
+
var targetRes = resolveVarValue(targetVar, depth + 1);
|
|
1056
|
+
return {
|
|
1057
|
+
isAlias: true,
|
|
1058
|
+
targetId: targetVar.id,
|
|
1059
|
+
targetName: targetVar.name,
|
|
1060
|
+
primitiveName: (targetRes && targetRes.primitiveName) ? targetRes.primitiveName : targetVar.name,
|
|
1061
|
+
resolvedValue: targetRes ? targetRes.resolvedValue : null,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (defaultVal && typeof defaultVal === "object" && "r" in defaultVal && "g" in defaultVal && "b" in defaultVal) {
|
|
1066
|
+
return { isAlias: false, resolvedValue: rgbToHex(defaultVal), primitiveName: v.name };
|
|
1067
|
+
}
|
|
1068
|
+
if (typeof defaultVal === "number" || typeof defaultVal === "string" || typeof defaultVal === "boolean") {
|
|
1069
|
+
return { isAlias: false, resolvedValue: defaultVal, primitiveName: v.name };
|
|
1070
|
+
}
|
|
1071
|
+
return { isAlias: false, resolvedValue: defaultVal, primitiveName: v.name };
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
for (var j = 0; j < vars.length; j++) {
|
|
1075
|
+
var vr = vars[j];
|
|
1076
|
+
if (!vr || !vr.id) continue;
|
|
1077
|
+
var res = resolveVarValue(vr, 0);
|
|
1078
|
+
var item = {
|
|
1079
|
+
id: vr.id,
|
|
1080
|
+
name: vr.name,
|
|
1081
|
+
resolvedType: vr.resolvedType,
|
|
1082
|
+
isAlias: res ? res.isAlias : false,
|
|
1083
|
+
targetName: res ? res.targetName : null,
|
|
1084
|
+
primitiveName: res ? res.primitiveName : vr.name,
|
|
1085
|
+
resolvedValue: res ? res.resolvedValue : null,
|
|
1086
|
+
};
|
|
1087
|
+
map.byId[vr.id] = item;
|
|
1088
|
+
map.byName[vr.name] = item;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
} catch(e) {}
|
|
1092
|
+
cachedVariableResolverMap = map;
|
|
1093
|
+
cachedVariableResolverMapTime = now;
|
|
1094
|
+
return map;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// ── Clean Variant & Component Properties Parser ──
|
|
1098
|
+
function cleanComponentProperties(node) {
|
|
1099
|
+
var out = { variant: null, props: null };
|
|
1100
|
+
if (!node) return out;
|
|
1101
|
+
|
|
1102
|
+
// 1. Native variantProperties on instance
|
|
1103
|
+
if (node.variantProperties && typeof node.variantProperties === "object") {
|
|
1104
|
+
var varKeys = Object.keys(node.variantProperties);
|
|
1105
|
+
if (varKeys.length > 0) {
|
|
1106
|
+
out.variant = {};
|
|
1107
|
+
for (var vk = 0; vk < varKeys.length; vk++) {
|
|
1108
|
+
out.variant[varKeys[vk]] = node.variantProperties[varKeys[vk]];
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// 2. Component Properties parsing (strip #ID suffix and separate variants vs normal props)
|
|
1114
|
+
if (node.componentProperties && typeof node.componentProperties === "object") {
|
|
1115
|
+
var rawProps = node.componentProperties;
|
|
1116
|
+
var propKeys = Object.keys(rawProps);
|
|
1117
|
+
if (propKeys.length > 0) {
|
|
1118
|
+
var cleanProps = {};
|
|
1119
|
+
var inferredVariants = out.variant || {};
|
|
1120
|
+
for (var pk = 0; pk < propKeys.length; pk++) {
|
|
1121
|
+
var rawKey = propKeys[pk];
|
|
1122
|
+
var propObj = rawProps[rawKey];
|
|
1123
|
+
if (!propObj) continue;
|
|
1124
|
+
var cleanKey = rawKey.split("#")[0].trim();
|
|
1125
|
+
if (propObj.type === "VARIANT") {
|
|
1126
|
+
inferredVariants[cleanKey] = propObj.value;
|
|
1127
|
+
} else {
|
|
1128
|
+
cleanProps[cleanKey] = propObj.value;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
if (Object.keys(cleanProps).length > 0) out.props = cleanProps;
|
|
1132
|
+
if (Object.keys(inferredVariants).length > 0) out.variant = inferredVariants;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return out;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// ── Infer Semantic Role ──
|
|
1139
|
+
function inferSemanticRole(node, info) {
|
|
1140
|
+
if (!node) return null;
|
|
1141
|
+
var name = (node.name || "").toLowerCase();
|
|
1142
|
+
var type = node.type;
|
|
1143
|
+
|
|
1144
|
+
if (isLikelyIcon(node) || name.indexOf("icon") !== -1) return "icon";
|
|
1145
|
+
if (name.indexOf("button") !== -1 || name.indexOf("btn") !== -1) return "button";
|
|
1146
|
+
if (name.indexOf("input") !== -1 || name.indexOf("textfield") !== -1 || name.indexOf("search") !== -1) return "input";
|
|
1147
|
+
if (name.indexOf("badge") !== -1 || name.indexOf("tag") !== -1 || name.indexOf("chip") !== -1 || name.indexOf("pill") !== -1) return "badge";
|
|
1148
|
+
if (name.indexOf("avatar") !== -1 || name.indexOf("userpic") !== -1 || name.indexOf("profile-pic") !== -1) return "avatar";
|
|
1149
|
+
if (name.indexOf("card") !== -1) return "card";
|
|
1150
|
+
if (name.indexOf("modal") !== -1 || name.indexOf("dialog") !== -1 || name.indexOf("popup") !== -1) return "modal";
|
|
1151
|
+
if (name.indexOf("checkbox") !== -1) return "checkbox";
|
|
1152
|
+
if (name.indexOf("switch") !== -1 || name.indexOf("toggle") !== -1) return "switch";
|
|
1153
|
+
if (name.indexOf("divider") !== -1 || name.indexOf("separator") !== -1) return "divider";
|
|
1154
|
+
if (name.indexOf("header") !== -1 || name.indexOf("navbar") !== -1 || name.indexOf("appbar") !== -1) return "header";
|
|
1155
|
+
if (name.indexOf("tab") !== -1) return "tab";
|
|
1156
|
+
|
|
1157
|
+
if (type === "INSTANCE") {
|
|
1158
|
+
if (info && info.componentSetName) {
|
|
1159
|
+
var setName = info.componentSetName.toLowerCase();
|
|
1160
|
+
if (setName.indexOf("button") !== -1) return "button";
|
|
1161
|
+
if (setName.indexOf("input") !== -1) return "input";
|
|
1162
|
+
if (setName.indexOf("badge") !== -1) return "badge";
|
|
1163
|
+
if (setName.indexOf("avatar") !== -1) return "avatar";
|
|
1164
|
+
if (setName.indexOf("icon") !== -1) return "icon";
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
871
1170
|
// Instance → main component resolution.
|
|
872
1171
|
//
|
|
873
1172
|
// `instance.mainComponent` throws under documentAccess: dynamic-page, so the
|
|
@@ -908,10 +1207,18 @@ async function resolveInstanceComponents(entries, apply) {
|
|
|
908
1207
|
}));
|
|
909
1208
|
for (var b = 0; b < batch.length; b++) {
|
|
910
1209
|
var main = mains[b];
|
|
911
|
-
|
|
912
|
-
var
|
|
913
|
-
if (
|
|
914
|
-
|
|
1210
|
+
var entryNode = batch[b].node;
|
|
1211
|
+
var entryInfo = batch[b].info;
|
|
1212
|
+
if (main) {
|
|
1213
|
+
var desc = describeMainComponent(main);
|
|
1214
|
+
if (apply) apply(entryInfo, desc);
|
|
1215
|
+
else Object.assign(entryInfo, desc);
|
|
1216
|
+
}
|
|
1217
|
+
var parsed = cleanComponentProperties(entryNode);
|
|
1218
|
+
if (parsed.variant) entryInfo.variant = parsed.variant;
|
|
1219
|
+
if (parsed.props) entryInfo.props = parsed.props;
|
|
1220
|
+
var role = inferSemanticRole(entryNode, entryInfo);
|
|
1221
|
+
if (role) entryInfo.role = role;
|
|
915
1222
|
resolved++;
|
|
916
1223
|
}
|
|
917
1224
|
}
|
|
@@ -1132,19 +1439,41 @@ function extractDesignTree(node, depth, maxDepth, detailLevel, filterInvisible,
|
|
|
1132
1439
|
try { if ("blendMode" in node && node.blendMode !== "NORMAL" && node.blendMode !== "PASS_THROUGH") info.blendMode = node.blendMode; } catch(e) {}
|
|
1133
1440
|
try { if ("clipsContent" in node && node.clipsContent) info.clipsContent = true; } catch(e) {}
|
|
1134
1441
|
|
|
1135
|
-
// ── Bound Variables (Design Tokens) — full
|
|
1136
|
-
if (isFull) try {
|
|
1442
|
+
// ── Bound Variables (Design Tokens) — full & compact ──
|
|
1443
|
+
if (isFull || isCompact) try {
|
|
1137
1444
|
if (node.boundVariables) {
|
|
1138
1445
|
var bv = {};
|
|
1139
1446
|
var bvKeys = Object.keys(node.boundVariables);
|
|
1447
|
+
var varMap = (walkState && walkState.variableMap) ? walkState.variableMap.byId : null;
|
|
1140
1448
|
for (var bvi = 0; bvi < bvKeys.length; bvi++) {
|
|
1141
1449
|
var bvk = bvKeys[bvi];
|
|
1142
1450
|
var binding = node.boundVariables[bvk];
|
|
1143
1451
|
if (binding) {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1452
|
+
var bindings = Array.isArray(binding) ? binding : [binding];
|
|
1453
|
+
var resolvedList = [];
|
|
1454
|
+
for (var bi = 0; bi < bindings.length; bi++) {
|
|
1455
|
+
var bObj = bindings[bi];
|
|
1456
|
+
if (!bObj || !bObj.id) continue;
|
|
1457
|
+
var vInfo = varMap && varMap[bObj.id] ? varMap[bObj.id] : null;
|
|
1458
|
+
if (vInfo) {
|
|
1459
|
+
var entry = { id: bObj.id, name: vInfo.name, type: vInfo.resolvedType };
|
|
1460
|
+
if (vInfo.resolvedValue !== null && vInfo.resolvedValue !== undefined) entry.value = vInfo.resolvedValue;
|
|
1461
|
+
if (vInfo.isAlias && vInfo.targetName) entry.aliasTarget = vInfo.targetName;
|
|
1462
|
+
resolvedList.push(entry);
|
|
1463
|
+
} else {
|
|
1464
|
+
resolvedList.push({ id: bObj.id });
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (resolvedList.length > 0) {
|
|
1468
|
+
bv[bvk] = Array.isArray(binding) ? resolvedList : resolvedList[0];
|
|
1469
|
+
var primaryEntry = resolvedList[0];
|
|
1470
|
+
if (primaryEntry && primaryEntry.name) {
|
|
1471
|
+
if (bvk === "fills" || bvk === "fill") info.fillToken = primaryEntry.name;
|
|
1472
|
+
else if (bvk === "strokes" || bvk === "stroke") info.strokeToken = primaryEntry.name;
|
|
1473
|
+
else if (bvk === "itemSpacing") info.gapToken = primaryEntry.name;
|
|
1474
|
+
else if (bvk === "paddingTop" || bvk === "paddingBottom" || bvk === "paddingLeft" || bvk === "paddingRight") info.paddingToken = primaryEntry.name;
|
|
1475
|
+
else if (bvk === "topLeftRadius" || bvk === "cornerRadius") info.radiusToken = primaryEntry.name;
|
|
1476
|
+
}
|
|
1148
1477
|
}
|
|
1149
1478
|
}
|
|
1150
1479
|
}
|
|
@@ -1210,6 +1539,8 @@ function extractDesignTree(node, depth, maxDepth, detailLevel, filterInvisible,
|
|
|
1210
1539
|
mode: node.layoutMode,
|
|
1211
1540
|
};
|
|
1212
1541
|
if (node.itemSpacing !== undefined && node.itemSpacing !== 0) layoutObj.itemSpacing = node.itemSpacing;
|
|
1542
|
+
if (info.gapToken) layoutObj.gapToken = info.gapToken;
|
|
1543
|
+
if (info.paddingToken) layoutObj.paddingToken = info.paddingToken;
|
|
1213
1544
|
if (node.primaryAxisAlignItems && node.primaryAxisAlignItems !== "MIN") layoutObj.align = node.primaryAxisAlignItems;
|
|
1214
1545
|
if (node.counterAxisAlignItems && node.counterAxisAlignItems !== "MIN") layoutObj.crossAlign = node.counterAxisAlignItems;
|
|
1215
1546
|
|
|
@@ -1259,7 +1590,6 @@ function extractDesignTree(node, depth, maxDepth, detailLevel, filterInvisible,
|
|
|
1259
1590
|
// ── Component-specific info ──
|
|
1260
1591
|
if (node.type === "COMPONENT" || node.type === "COMPONENT_SET") {
|
|
1261
1592
|
try { info.description = node.description; } catch(e) {}
|
|
1262
|
-
// Expose component property definitions for COMPONENT/COMPONENT_SET
|
|
1263
1593
|
try {
|
|
1264
1594
|
if (node.componentPropertyDefinitions) {
|
|
1265
1595
|
var defs = node.componentPropertyDefinitions;
|
|
@@ -1276,27 +1606,17 @@ function extractDesignTree(node, depth, maxDepth, detailLevel, filterInvisible,
|
|
|
1276
1606
|
} catch(e) {}
|
|
1277
1607
|
}
|
|
1278
1608
|
if (node.type === "INSTANCE") {
|
|
1279
|
-
|
|
1280
|
-
|
|
1609
|
+
var parsedProps = cleanComponentProperties(node);
|
|
1610
|
+
if (parsedProps.variant) info.variant = parsedProps.variant;
|
|
1611
|
+
if (parsedProps.props) info.props = parsedProps.props;
|
|
1281
1612
|
if (instanceCollector) instanceCollector.push({ info: info, node: node });
|
|
1282
1613
|
try { if (node.overrides && node.overrides.length) info.overrideCount = node.overrides.length; } catch(e) {}
|
|
1283
|
-
// Issue #4: expose explicit component property values on this instance
|
|
1284
|
-
try {
|
|
1285
|
-
if (node.componentProperties) {
|
|
1286
|
-
var props = node.componentProperties;
|
|
1287
|
-
var propKeys = Object.keys(props);
|
|
1288
|
-
if (propKeys.length > 0) {
|
|
1289
|
-
info.componentPropertyValues = {};
|
|
1290
|
-
for (var pi = 0; pi < propKeys.length; pi++) {
|
|
1291
|
-
var pk = propKeys[pi];
|
|
1292
|
-
var pv = props[pk];
|
|
1293
|
-
info.componentPropertyValues[pk] = { type: pv.type, value: pv.value };
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
} catch(e) {}
|
|
1298
1614
|
}
|
|
1299
1615
|
|
|
1616
|
+
// ── Semantic Role ──
|
|
1617
|
+
var semanticRole = inferSemanticRole(node, info);
|
|
1618
|
+
if (semanticRole) info.role = semanticRole;
|
|
1619
|
+
|
|
1300
1620
|
// ── VECTOR / BOOLEAN_OPERATION ──
|
|
1301
1621
|
if (node.type === "VECTOR" || node.type === "BOOLEAN_OPERATION") {
|
|
1302
1622
|
try { if (node.vectorPaths) info.pathCount = node.vectorPaths.length; } catch(e) {}
|
|
@@ -2342,6 +2662,21 @@ handlers.swapComponent = async function(params) {
|
|
|
2342
2662
|
};
|
|
2343
2663
|
};
|
|
2344
2664
|
|
|
2665
|
+
// ─── ALIASES FOR WRITE HANDLERS ──────────────────────────────────────────────
|
|
2666
|
+
handlers.list_pages = handlers.listPages;
|
|
2667
|
+
handlers.set_page = handlers.setPage;
|
|
2668
|
+
handlers.create_page = handlers.createPage;
|
|
2669
|
+
handlers.list_components = handlers.listComponents;
|
|
2670
|
+
handlers.load_all_pages_async = handlers.loadAllPagesAsync;
|
|
2671
|
+
handlers.loadAllPages = handlers.loadAllPagesAsync;
|
|
2672
|
+
handlers.load_all_pages = handlers.loadAllPagesAsync;
|
|
2673
|
+
handlers.set_component_properties = handlers.setComponentProperties;
|
|
2674
|
+
handlers.get_component_properties = handlers.getComponentProperties;
|
|
2675
|
+
handlers.swap_component = handlers.swapComponent;
|
|
2676
|
+
handlers.update = handlers.modify;
|
|
2677
|
+
handlers.remove = handlers["delete"];
|
|
2678
|
+
|
|
2679
|
+
|
|
2345
2680
|
|
|
2346
2681
|
// ── plugin-src/handlers-library.js ──
|
|
2347
2682
|
// ─── DESIGN LIBRARY HANDLERS ──────────────────────────────────────────────────
|
|
@@ -2472,6 +2807,11 @@ handlers.get_library_tokens = async function() {
|
|
|
2472
2807
|
return { libraryId: lib.id, colors: colors, textStyles: textStyles };
|
|
2473
2808
|
};
|
|
2474
2809
|
|
|
2810
|
+
// ─── ALIASES FOR LIBRARY HANDLERS ─────────────────────────────────────────────
|
|
2811
|
+
handlers.ensureLibrary = handlers.ensure_library;
|
|
2812
|
+
handlers.getLibraryTokens = handlers.get_library_tokens;
|
|
2813
|
+
|
|
2814
|
+
|
|
2475
2815
|
|
|
2476
2816
|
// ── plugin-src/handlers-read.js ──
|
|
2477
2817
|
// ─── READ HANDLERS ────────────────────────────────────────────────────────────
|
|
@@ -2482,10 +2822,16 @@ handlers.get_library_tokens = async function() {
|
|
|
2482
2822
|
// `absolute: true` forces absoluteBoundingBox on every node.
|
|
2483
2823
|
var DEFAULT_NODE_BUDGET = 3000;
|
|
2484
2824
|
|
|
2485
|
-
function
|
|
2825
|
+
async function makeWalkStateAsync(params) {
|
|
2486
2826
|
var p = params || {};
|
|
2487
2827
|
var budget = (p.maxNodes !== undefined && Number(p.maxNodes) > 0) ? Number(p.maxNodes) : DEFAULT_NODE_BUDGET;
|
|
2488
|
-
|
|
2828
|
+
var varMap = null;
|
|
2829
|
+
try {
|
|
2830
|
+
if (typeof buildVariableResolverMapAsync === "function") {
|
|
2831
|
+
varMap = await buildVariableResolverMapAsync();
|
|
2832
|
+
}
|
|
2833
|
+
} catch(e) {}
|
|
2834
|
+
return { remaining: budget, budget: budget, truncated: false, absolute: p.absolute === true, variableMap: varMap };
|
|
2489
2835
|
}
|
|
2490
2836
|
|
|
2491
2837
|
function walkStateMeta(walkState) {
|
|
@@ -2518,7 +2864,7 @@ handlers.get_selection = async function(params) {
|
|
|
2518
2864
|
var filterInvisible = !(params && params.includeHidden === true);
|
|
2519
2865
|
var tokenCollector = (detailLevel !== "minimal") ? { colors: new Set(), fonts: new Set(), sizes: new Set() } : null;
|
|
2520
2866
|
var instanceCollector = (detailLevel !== "minimal") ? [] : null;
|
|
2521
|
-
var walkState =
|
|
2867
|
+
var walkState = await makeWalkStateAsync(params);
|
|
2522
2868
|
var trees = nodes.map(function(n) { return extractDesignTree(n, 0, maxDepth, detailLevel, filterInvisible, tokenCollector, instanceCollector, walkState); });
|
|
2523
2869
|
// mainComponent is async-only under documentAccess: dynamic-page — resolve
|
|
2524
2870
|
// every collected INSTANCE after the (synchronous) tree walk.
|
|
@@ -2559,7 +2905,7 @@ handlers.get_design = async function(params) {
|
|
|
2559
2905
|
try {
|
|
2560
2906
|
var tokenCollector = (detailLevel !== "minimal") ? { colors: new Set(), fonts: new Set(), sizes: new Set() } : null;
|
|
2561
2907
|
var instanceCollector = (detailLevel !== "minimal") ? [] : null;
|
|
2562
|
-
var walkState =
|
|
2908
|
+
var walkState = await makeWalkStateAsync(p);
|
|
2563
2909
|
var tree = extractDesignTree(root, 0, maxDepth, detailLevel, filterInvisible, tokenCollector, instanceCollector, walkState);
|
|
2564
2910
|
var instanceInfo = await resolveInstanceComponents(instanceCollector);
|
|
2565
2911
|
|
|
@@ -3306,19 +3652,54 @@ handlers.index_scan = async function() {
|
|
|
3306
3652
|
};
|
|
3307
3653
|
};
|
|
3308
3654
|
|
|
3655
|
+
// ─── ALIASES FOR READ HANDLERS ────────────────────────────────────────────────
|
|
3656
|
+
handlers.getSelection = handlers.get_selection;
|
|
3657
|
+
handlers.selection = handlers.get_selection;
|
|
3658
|
+
handlers.getDesign = handlers.get_design;
|
|
3659
|
+
handlers.scanDesign = handlers.scan_design;
|
|
3660
|
+
handlers.searchNodes = handlers.search_nodes;
|
|
3661
|
+
handlers.getPageNodes = handlers.get_page_nodes;
|
|
3662
|
+
handlers.pageNodes = handlers.get_page_nodes;
|
|
3663
|
+
handlers.capture = handlers.screenshot;
|
|
3664
|
+
handlers.takeScreenshot = handlers.screenshot;
|
|
3665
|
+
handlers.exportSvg = handlers.export_svg;
|
|
3666
|
+
handlers.exportImage = handlers.export_image;
|
|
3667
|
+
handlers.indexScan = handlers.index_scan;
|
|
3668
|
+
|
|
3669
|
+
|
|
3309
3670
|
|
|
3310
3671
|
// ── plugin-src/handlers-read-detail.js ──
|
|
3311
3672
|
// ─── READ DETAIL HANDLERS ─────────────────────────────────────────────────────
|
|
3312
3673
|
|
|
3313
3674
|
// get_node_detail — CSS-like properties for a single node (no tree traversal)
|
|
3314
3675
|
handlers.get_node_detail = async function(params) {
|
|
3315
|
-
//
|
|
3316
|
-
|
|
3317
|
-
|
|
3676
|
+
// Batch support: if nodeIds array is provided, inspect multiple nodes in one call
|
|
3677
|
+
if (params && (Array.isArray(params.nodeIds) || Array.isArray(params.ids))) {
|
|
3678
|
+
var ids = params.nodeIds || params.ids;
|
|
3679
|
+
var results = [];
|
|
3680
|
+
for (var bi = 0; bi < ids.length; bi++) {
|
|
3681
|
+
try {
|
|
3682
|
+
var nd = await handlers.get_node_detail({ id: ids[bi] });
|
|
3683
|
+
results.push(nd);
|
|
3684
|
+
} catch (e) {
|
|
3685
|
+
results.push({ id: ids[bi], error: e.message });
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
return { nodes: results, count: results.length };
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
// Accept id, nodeId, node_id, targetId, target_id, name, nodeName, node_name — try ID first then name fallback
|
|
3692
|
+
var id = params ? (params.id || params.nodeId || params.node_id || params.targetId || params.target_id) : null;
|
|
3693
|
+
var nodeName = params ? (params.name || params.nodeName || params.node_name) : null;
|
|
3318
3694
|
var node = null;
|
|
3319
3695
|
if (id) node = await findNodeByIdAsync(id);
|
|
3320
3696
|
if (!node && nodeName) node = findNodeByName(nodeName);
|
|
3321
|
-
if (!node)
|
|
3697
|
+
if (!node) {
|
|
3698
|
+
var errMsg = typeof getNodeNotFoundContext === "function"
|
|
3699
|
+
? getNodeNotFoundContext(id, nodeName)
|
|
3700
|
+
: ("Node not found: " + (id || nodeName || "no id/name given") + ". Use figma_read get_page_nodes to get current node IDs.");
|
|
3701
|
+
throw new Error(errMsg);
|
|
3702
|
+
}
|
|
3322
3703
|
|
|
3323
3704
|
var detail = {
|
|
3324
3705
|
id: node.id, name: node.name, type: node.type,
|
|
@@ -3550,6 +3931,25 @@ handlers.get_node_detail = async function(params) {
|
|
|
3550
3931
|
}
|
|
3551
3932
|
} catch(e) {}
|
|
3552
3933
|
|
|
3934
|
+
// Attach direct semantic token names on detail
|
|
3935
|
+
try {
|
|
3936
|
+
if (detail.boundVariables) {
|
|
3937
|
+
var bvKeys = Object.keys(detail.boundVariables);
|
|
3938
|
+
for (var bvi = 0; bvi < bvKeys.length; bvi++) {
|
|
3939
|
+
var bvk = bvKeys[bvi];
|
|
3940
|
+
var bEntry = detail.boundVariables[bvk];
|
|
3941
|
+
var pName = (bEntry && typeof bEntry === "object") ? (Array.isArray(bEntry) ? (bEntry[0] && bEntry[0].name) : bEntry.name) : null;
|
|
3942
|
+
if (pName) {
|
|
3943
|
+
if (bvk === "fills" || bvk === "fill") detail.fillToken = pName;
|
|
3944
|
+
else if (bvk === "strokes" || bvk === "stroke") detail.strokeToken = pName;
|
|
3945
|
+
else if (bvk === "itemSpacing") detail.gapToken = pName;
|
|
3946
|
+
else if (bvk === "paddingTop" || bvk === "paddingBottom" || bvk === "paddingLeft" || bvk === "paddingRight") detail.paddingToken = pName;
|
|
3947
|
+
else if (bvk === "topLeftRadius" || bvk === "cornerRadius") detail.radiusToken = pName;
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
} catch(e) {}
|
|
3952
|
+
|
|
3553
3953
|
// P4: Resolve style IDs → name + values (not just opaque IDs)
|
|
3554
3954
|
try {
|
|
3555
3955
|
if (node.textStyleId && typeof node.textStyleId === "string") {
|
|
@@ -3576,12 +3976,15 @@ handlers.get_node_detail = async function(params) {
|
|
|
3576
3976
|
try { if (node.strokeStyleId && typeof node.strokeStyleId === "string") detail.strokeStyleId = node.strokeStyleId; } catch(e) {}
|
|
3577
3977
|
try { if (node.effectStyleId && typeof node.effectStyleId === "string") detail.effectStyleId = node.effectStyleId; } catch(e) {}
|
|
3578
3978
|
|
|
3579
|
-
// Instance: source component reference + property values + overrides + P5 variantLabel
|
|
3979
|
+
// Instance: source component reference + property values + overrides + P5 variantLabel + clean variant & props
|
|
3580
3980
|
if (node.type === "INSTANCE") {
|
|
3581
|
-
// mainComponent is async-only under documentAccess: dynamic-page.
|
|
3582
|
-
// describeMainComponent yields componentId/componentName/componentSetName/variantLabel.
|
|
3583
3981
|
var instComp = await getMainComponentSafe(node);
|
|
3584
3982
|
if (instComp) Object.assign(detail, describeMainComponent(instComp));
|
|
3983
|
+
var parsed = typeof cleanComponentProperties === "function" ? cleanComponentProperties(node) : null;
|
|
3984
|
+
if (parsed) {
|
|
3985
|
+
if (parsed.variant) detail.variant = parsed.variant;
|
|
3986
|
+
if (parsed.props) detail.props = parsed.props;
|
|
3987
|
+
}
|
|
3585
3988
|
// P3: Full override list instead of just count
|
|
3586
3989
|
try {
|
|
3587
3990
|
if (node.overrides && node.overrides.length) {
|
|
@@ -3607,6 +4010,10 @@ handlers.get_node_detail = async function(params) {
|
|
|
3607
4010
|
} catch(e) {}
|
|
3608
4011
|
}
|
|
3609
4012
|
|
|
4013
|
+
// Semantic Role
|
|
4014
|
+
var semanticRole = typeof inferSemanticRole === "function" ? inferSemanticRole(node, detail) : null;
|
|
4015
|
+
if (semanticRole) detail.role = semanticRole;
|
|
4016
|
+
|
|
3610
4017
|
// Children count + text content summary
|
|
3611
4018
|
if (node && typeof node === "object" && "children" in node && Array.isArray(node.children)) {
|
|
3612
4019
|
detail.childCount = node.children.length;
|
|
@@ -3714,8 +4121,8 @@ handlers.get_css = async function(params) {
|
|
|
3714
4121
|
// Returns: flex layout semantics, token-resolved colors, typography, component instances
|
|
3715
4122
|
// Much more code-ready than get_design (raw tree) or get_css (single string)
|
|
3716
4123
|
handlers.get_design_context = async function(params) {
|
|
3717
|
-
var id = params ? (params.id || params.nodeId) : null;
|
|
3718
|
-
var nodeName = params ? (params.name || params.nodeName) : null;
|
|
4124
|
+
var id = params ? (params.id || params.nodeId || params.node_id || params.targetId || params.target_id) : null;
|
|
4125
|
+
var nodeName = params ? (params.name || params.nodeName || params.node_name) : null;
|
|
3719
4126
|
var node = null;
|
|
3720
4127
|
if (id) node = await findNodeByIdAsync(id);
|
|
3721
4128
|
if (!node && nodeName) node = findNodeByName(nodeName);
|
|
@@ -3724,7 +4131,12 @@ handlers.get_design_context = async function(params) {
|
|
|
3724
4131
|
var sel = figma.currentPage.selection;
|
|
3725
4132
|
node = sel && sel.length > 0 ? sel[0] : null;
|
|
3726
4133
|
}
|
|
3727
|
-
if (!node)
|
|
4134
|
+
if (!node) {
|
|
4135
|
+
var errMsg = (id || nodeName) && typeof getNodeNotFoundContext === "function"
|
|
4136
|
+
? getNodeNotFoundContext(id, nodeName)
|
|
4137
|
+
: "No node specified and nothing selected. Pass nodeId or select a node in Figma.";
|
|
4138
|
+
throw new Error(errMsg);
|
|
4139
|
+
}
|
|
3728
4140
|
|
|
3729
4141
|
// Build variable name lookup: variableId → name
|
|
3730
4142
|
// Use getLocalVariablesAsync (single call) instead of per-ID fetches — avoids O(n²) await loop
|
|
@@ -3789,6 +4201,26 @@ handlers.get_design_context = async function(params) {
|
|
|
3789
4201
|
var fillVal = resolveFill(nd);
|
|
3790
4202
|
if (fillVal) ctx.fill = fillVal;
|
|
3791
4203
|
|
|
4204
|
+
// Direct token bindings
|
|
4205
|
+
try {
|
|
4206
|
+
if (nd.boundVariables) {
|
|
4207
|
+
var bvKeys = Object.keys(nd.boundVariables);
|
|
4208
|
+
for (var bvi = 0; bvi < bvKeys.length; bvi++) {
|
|
4209
|
+
var bvk = bvKeys[bvi];
|
|
4210
|
+
var binding = nd.boundVariables[bvk];
|
|
4211
|
+
var bindId = binding ? (Array.isArray(binding) ? (binding[0] ? binding[0].id : null) : binding.id) : null;
|
|
4212
|
+
if (bindId && varMap[bindId]) {
|
|
4213
|
+
var vName = varMap[bindId].name;
|
|
4214
|
+
if (bvk === "fills" || bvk === "fill") ctx.fillToken = vName;
|
|
4215
|
+
else if (bvk === "strokes" || bvk === "stroke") ctx.strokeToken = vName;
|
|
4216
|
+
else if (bvk === "itemSpacing") ctx.gapToken = vName;
|
|
4217
|
+
else if (bvk === "paddingTop" || bvk === "paddingBottom" || bvk === "paddingLeft" || bvk === "paddingRight") ctx.paddingToken = vName;
|
|
4218
|
+
else if (bvk === "topLeftRadius" || bvk === "cornerRadius") ctx.radiusToken = vName;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
} catch(e) {}
|
|
4223
|
+
|
|
3792
4224
|
// Stroke
|
|
3793
4225
|
try {
|
|
3794
4226
|
if (nd.strokes && nd.strokes.length && nd.strokes[0].type === "SOLID") {
|
|
@@ -3841,16 +4273,17 @@ handlers.get_design_context = async function(params) {
|
|
|
3841
4273
|
// documentAccess: dynamic-page, so collect now and resolve after the walk.
|
|
3842
4274
|
if (nd.type === "INSTANCE") {
|
|
3843
4275
|
ctxInstances.push({ info: ctx, node: nd });
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
if (pks.length) ctx.component = { properties: props };
|
|
3850
|
-
}
|
|
3851
|
-
} catch(e) {}
|
|
4276
|
+
var parsedProps = typeof cleanComponentProperties === "function" ? cleanComponentProperties(nd) : null;
|
|
4277
|
+
if (parsedProps) {
|
|
4278
|
+
if (parsedProps.variant) ctx.variant = parsedProps.variant;
|
|
4279
|
+
if (parsedProps.props) ctx.props = parsedProps.props;
|
|
4280
|
+
}
|
|
3852
4281
|
}
|
|
3853
4282
|
|
|
4283
|
+
// Role
|
|
4284
|
+
var sRole = typeof inferSemanticRole === "function" ? inferSemanticRole(nd, ctx) : null;
|
|
4285
|
+
if (sRole) ctx.role = sRole;
|
|
4286
|
+
|
|
3854
4287
|
// Children (limited depth to avoid token overflow)
|
|
3855
4288
|
if (depth < 4 && nd.children && nd.children.length) {
|
|
3856
4289
|
ctx.children = [];
|
|
@@ -3907,9 +4340,23 @@ handlers.get_design_context = async function(params) {
|
|
|
3907
4340
|
|
|
3908
4341
|
// Aliases for developer convenience & MCP tool naming compatibility
|
|
3909
4342
|
handlers.inspect_node = handlers.get_design_context;
|
|
4343
|
+
handlers.inspectNode = handlers.get_design_context;
|
|
3910
4344
|
handlers.inspect = handlers.get_design_context;
|
|
4345
|
+
handlers.getDesignContext = handlers.get_design_context;
|
|
4346
|
+
handlers.designContext = handlers.get_design_context;
|
|
4347
|
+
handlers.getCss = handlers.get_css;
|
|
4348
|
+
handlers.css = handlers.get_css;
|
|
4349
|
+
handlers.get_node = handlers.get_node_detail;
|
|
4350
|
+
handlers.getNode = handlers.get_node_detail;
|
|
4351
|
+
handlers.get_node_detail = handlers.get_node_detail;
|
|
4352
|
+
handlers.getNodeDetail = handlers.get_node_detail;
|
|
3911
4353
|
handlers.get_node_info = handlers.get_node_detail;
|
|
4354
|
+
handlers.getNodeInfo = handlers.get_node_detail;
|
|
3912
4355
|
handlers.node_detail = handlers.get_node_detail;
|
|
4356
|
+
handlers.nodeDetail = handlers.get_node_detail;
|
|
4357
|
+
handlers.node_info = handlers.get_node_detail;
|
|
4358
|
+
handlers.nodeInfo = handlers.get_node_detail;
|
|
4359
|
+
|
|
3913
4360
|
|
|
3914
4361
|
// get_component_map — list every component instance in a frame with variant properties
|
|
3915
4362
|
// Use this to map Figma components to their code equivalents
|
|
@@ -4220,8 +4667,21 @@ handlers.get_variables = async function() {
|
|
|
4220
4667
|
};
|
|
4221
4668
|
|
|
4222
4669
|
handlers.get_variable_tokens = handlers.get_variables;
|
|
4670
|
+
handlers.getVariableTokens = handlers.get_variables;
|
|
4223
4671
|
handlers.get_tokens = handlers.get_variables;
|
|
4672
|
+
handlers.getTokens = handlers.get_variables;
|
|
4224
4673
|
handlers.tokens = handlers.get_variables;
|
|
4674
|
+
handlers.variables = handlers.get_variables;
|
|
4675
|
+
handlers.getVariables = handlers.get_variables;
|
|
4676
|
+
handlers.getStyles = handlers.get_styles;
|
|
4677
|
+
handlers.styles = handlers.get_styles;
|
|
4678
|
+
handlers.getLocalComponents = handlers.get_local_components;
|
|
4679
|
+
handlers.localComponents = handlers.get_local_components;
|
|
4680
|
+
handlers.getViewport = handlers.get_viewport;
|
|
4681
|
+
handlers.setViewport = handlers.set_viewport;
|
|
4682
|
+
handlers.getComponentMap = handlers.get_component_map;
|
|
4683
|
+
handlers.getUnmappedComponents = handlers.get_unmapped_components;
|
|
4684
|
+
|
|
4225
4685
|
|
|
4226
4686
|
// ─── EXPORT ASSETS (Batch Icon & Image Extractor) ────────────────────────────
|
|
4227
4687
|
handlers.export_assets = async function(params) {
|
|
@@ -4348,6 +4808,9 @@ handlers.export_assets = async function(params) {
|
|
|
4348
4808
|
};
|
|
4349
4809
|
};
|
|
4350
4810
|
|
|
4811
|
+
handlers.exportAssets = handlers.export_assets;
|
|
4812
|
+
|
|
4813
|
+
|
|
4351
4814
|
|
|
4352
4815
|
// ── plugin-src/handlers-tokens.js ──
|
|
4353
4816
|
// ─── DESIGN TOKEN OPERATIONS (v1.7.0) ───────────────────────────────────────
|
|
@@ -5471,6 +5934,29 @@ handlers.removeComponentProperty = async function(params) {
|
|
|
5471
5934
|
};
|
|
5472
5935
|
};
|
|
5473
5936
|
|
|
5937
|
+
// ─── ALIASES FOR TOKEN & COMPONENT PROPERTY HANDLERS ─────────────────────────
|
|
5938
|
+
handlers.create_variable_collection = handlers.createVariableCollection;
|
|
5939
|
+
handlers.create_variable = handlers.createVariable;
|
|
5940
|
+
handlers.add_variable_mode = handlers.addVariableMode;
|
|
5941
|
+
handlers.rename_variable_mode = handlers.renameVariableMode;
|
|
5942
|
+
handlers.remove_variable_mode = handlers.removeVariableMode;
|
|
5943
|
+
handlers.set_variable_value = handlers.setVariableValue;
|
|
5944
|
+
handlers.set_frame_variable_mode = handlers.setFrameVariableMode;
|
|
5945
|
+
handlers.clear_frame_variable_mode = handlers.clearFrameVariableMode;
|
|
5946
|
+
handlers.apply_variable = handlers.applyVariable;
|
|
5947
|
+
handlers.create_paint_style = handlers.createPaintStyle;
|
|
5948
|
+
handlers.create_text_style = handlers.createTextStyle;
|
|
5949
|
+
handlers.create_component = handlers.createComponent;
|
|
5950
|
+
handlers.modify_variable = handlers.modifyVariable;
|
|
5951
|
+
handlers.setup_design_tokens = handlers.setupDesignTokens;
|
|
5952
|
+
handlers.apply_text_style = handlers.applyTextStyle;
|
|
5953
|
+
handlers.add_component_property = handlers.addComponentProperty;
|
|
5954
|
+
handlers.bind_component_property_to_text = handlers.bindComponentPropertyToText;
|
|
5955
|
+
handlers.bind_component_property = handlers.bindComponentProperty;
|
|
5956
|
+
handlers.unbind_component_property = handlers.unbindComponentProperty;
|
|
5957
|
+
handlers.remove_component_property = handlers.removeComponentProperty;
|
|
5958
|
+
|
|
5959
|
+
|
|
5474
5960
|
|
|
5475
5961
|
// ── plugin-src/handlers-write-ops.js ──
|
|
5476
5962
|
// ─── NEW WRITE OPERATIONS ────────────────────────────────────────────────────
|
|
@@ -5568,6 +6054,8 @@ handlers.set_selection = async function(params) {
|
|
|
5568
6054
|
return { selected: nodes.map(nodeToInfo) };
|
|
5569
6055
|
};
|
|
5570
6056
|
|
|
6057
|
+
handlers.setSelection = handlers.set_selection;
|
|
6058
|
+
|
|
5571
6059
|
// batch — execute multiple operations in one call
|
|
5572
6060
|
// Supported operations: create, modify, delete (single or batch ids), append, clone
|
|
5573
6061
|
handlers.batch = async function(params) {
|
|
@@ -5579,7 +6067,7 @@ handlers.batch = async function(params) {
|
|
|
5579
6067
|
var results = [];
|
|
5580
6068
|
for (var i = 0; i < operations.length; i++) {
|
|
5581
6069
|
var op = operations[i];
|
|
5582
|
-
var handler = handlers[op.operation];
|
|
6070
|
+
var handler = typeof resolveOperationHandler === "function" ? resolveOperationHandler(op.operation) : handlers[op.operation];
|
|
5583
6071
|
if (!handler) {
|
|
5584
6072
|
results.push({ index: i, operation: op.operation, success: false, error: "Unknown operation" });
|
|
5585
6073
|
continue;
|
|
@@ -5595,6 +6083,7 @@ handlers.batch = async function(params) {
|
|
|
5595
6083
|
};
|
|
5596
6084
|
|
|
5597
6085
|
|
|
6086
|
+
|
|
5598
6087
|
// ── plugin-src/main.js ──
|
|
5599
6088
|
// ─── PLUGIN ENTRY POINT ───────────────────────────────────────────────────────
|
|
5600
6089
|
|
|
@@ -5921,7 +6410,9 @@ figma.ui.onmessage = async (request) => {
|
|
|
5921
6410
|
}
|
|
5922
6411
|
|
|
5923
6412
|
const { id, operation, params } = request;
|
|
5924
|
-
const handler =
|
|
6413
|
+
const handler = typeof resolveOperationHandler === "function"
|
|
6414
|
+
? resolveOperationHandler(operation)
|
|
6415
|
+
: handlers[operation];
|
|
5925
6416
|
|
|
5926
6417
|
if (!handler) {
|
|
5927
6418
|
figma.ui.postMessage({
|