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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.4",
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-29T14:34:20.483Z
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
- if (figma.currentPage.id === id) return figma.currentPage;
353
- if (figma.root.id === id) return figma.root;
354
- if (nodeCache.has(id)) {
355
- var cached = nodeCache.get(id);
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
- if (figma.currentPage.selection[i].id === id) return cacheNode(figma.currentPage.selection[i]);
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
- if (figma.currentPage.id === id) return figma.currentPage;
375
- if (figma.root.id === id) return figma.root;
376
- if (nodeCache.has(id)) {
377
- var cached = nodeCache.get(id);
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
- // Try synchronous lookup first (0ms)
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
- // Fallback to async
432
+
433
+ // 2. Try async lookup
389
434
  try {
390
- var node = await figma.getNodeByIdAsync(id);
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 => n.name === name);
406
- if (found) cacheNode(found);
407
- return found;
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
- var id = params.id || params.nodeId || params.targetId;
412
- var name = params.name || params.nodeName;
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
- if (!main) continue;
912
- var desc = describeMainComponent(main);
913
- if (apply) apply(batch[b].info, desc);
914
- else Object.assign(batch[b].info, desc);
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 only ──
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
- if (Array.isArray(binding)) {
1145
- bv[bvk] = binding.map(function(b) { return b ? b.id : null; });
1146
- } else {
1147
- bv[bvk] = binding.id || null;
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
- // Issue #2: expose source component reference. mainComponent is async-only
1280
- // under documentAccess: dynamic-page, so defer to the caller's collector.
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 makeWalkState(params) {
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
- return { remaining: budget, budget: budget, truncated: false, absolute: p.absolute === true };
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 = makeWalkState(params);
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 = makeWalkState(p);
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
- // Accept id, nodeId, name, nodeName — try ID first then name fallback
3316
- var id = params ? (params.id || params.nodeId) : null;
3317
- var nodeName = params ? (params.name || params.nodeName) : null;
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) throw new Error("Node not found: " + (id || nodeName || "no id/name given") + ". Use figma_read get_page_nodes to get current node IDs.");
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) throw new Error("No node specified and nothing selected. Pass nodeId or select a 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
- try {
3845
- if (nd.componentProperties) {
3846
- var props = {};
3847
- var pks = Object.keys(nd.componentProperties);
3848
- for (var pi = 0; pi < pks.length; pi++) props[pks[pi]] = nd.componentProperties[pks[pi]].value;
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 = handlers[operation];
6413
+ const handler = typeof resolveOperationHandler === "function"
6414
+ ? resolveOperationHandler(operation)
6415
+ : handlers[operation];
5925
6416
 
5926
6417
  if (!handler) {
5927
6418
  figma.ui.postMessage({