figma-rust-mcp 3.0.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-rust-mcp",
3
- "version": "3.0.3",
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-29T13:39:20.933Z
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) {}
@@ -1348,7 +1668,7 @@ const handlers = {};
1348
1668
 
1349
1669
  handlers.status = async () => ({
1350
1670
  connected: true,
1351
- version: "3.0.3",
1671
+ version: "3.0.4",
1352
1672
  fileName: figma.root.name,
1353
1673
  currentPage: figma.currentPage.name,
1354
1674
  pageCount: figma.root.children.length,
@@ -1414,13 +1734,15 @@ handlers.createPage = async (params) => {
1414
1734
  };
1415
1735
 
1416
1736
  handlers.query = async (params) => {
1417
- var type = params.type, name = params.name, id = params.id;
1737
+ params = params || {};
1738
+ var type = params.type;
1739
+ var name = params.name || params.nodeName || params.query || params.q;
1740
+ var id = params.id || params.nodeId;
1418
1741
  var parentId = params.parentId, limit = params.limit || 500;
1419
1742
  if (id) {
1420
1743
  const n = await findNodeByIdAsync(id);
1421
1744
  return n ? [nodeToInfo(n)] : [];
1422
1745
  }
1423
- if (!type && !name) throw new Error("query requires at least one of: type, name, id");
1424
1746
  // BUG-03: parentId filter scopes search to a subtree; default cap raised 100 → 500.
1425
1747
  var scope = figma.currentPage;
1426
1748
  if (parentId) {
@@ -1429,10 +1751,14 @@ handlers.query = async (params) => {
1429
1751
  if (typeof parent.findAll !== "function") return [];
1430
1752
  scope = parent;
1431
1753
  }
1754
+ if (!type && !name) {
1755
+ var allChildren = scope.children || [];
1756
+ return allChildren.slice(0, limit).map(nodeToInfo);
1757
+ }
1432
1758
  const results = scope.findAll(n => {
1433
- if (type && name) return n.type === type && n.name === name;
1759
+ if (type && name) return n.type === type && (n.name === name || (typeof name === "string" && n.name.toLowerCase().includes(name.toLowerCase())));
1434
1760
  if (type) return n.type === type;
1435
- return n.name === name;
1761
+ return n.name === name || (typeof name === "string" && n.name.toLowerCase().includes(name.toLowerCase()));
1436
1762
  });
1437
1763
  return results.slice(0, limit).map(nodeToInfo);
1438
1764
  };
@@ -2336,6 +2662,21 @@ handlers.swapComponent = async function(params) {
2336
2662
  };
2337
2663
  };
2338
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
+
2339
2680
 
2340
2681
  // ── plugin-src/handlers-library.js ──
2341
2682
  // ─── DESIGN LIBRARY HANDLERS ──────────────────────────────────────────────────
@@ -2466,6 +2807,11 @@ handlers.get_library_tokens = async function() {
2466
2807
  return { libraryId: lib.id, colors: colors, textStyles: textStyles };
2467
2808
  };
2468
2809
 
2810
+ // ─── ALIASES FOR LIBRARY HANDLERS ─────────────────────────────────────────────
2811
+ handlers.ensureLibrary = handlers.ensure_library;
2812
+ handlers.getLibraryTokens = handlers.get_library_tokens;
2813
+
2814
+
2469
2815
 
2470
2816
  // ── plugin-src/handlers-read.js ──
2471
2817
  // ─── READ HANDLERS ────────────────────────────────────────────────────────────
@@ -2476,10 +2822,16 @@ handlers.get_library_tokens = async function() {
2476
2822
  // `absolute: true` forces absoluteBoundingBox on every node.
2477
2823
  var DEFAULT_NODE_BUDGET = 3000;
2478
2824
 
2479
- function makeWalkState(params) {
2825
+ async function makeWalkStateAsync(params) {
2480
2826
  var p = params || {};
2481
2827
  var budget = (p.maxNodes !== undefined && Number(p.maxNodes) > 0) ? Number(p.maxNodes) : DEFAULT_NODE_BUDGET;
2482
- 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 };
2483
2835
  }
2484
2836
 
2485
2837
  function walkStateMeta(walkState) {
@@ -2512,7 +2864,7 @@ handlers.get_selection = async function(params) {
2512
2864
  var filterInvisible = !(params && params.includeHidden === true);
2513
2865
  var tokenCollector = (detailLevel !== "minimal") ? { colors: new Set(), fonts: new Set(), sizes: new Set() } : null;
2514
2866
  var instanceCollector = (detailLevel !== "minimal") ? [] : null;
2515
- var walkState = makeWalkState(params);
2867
+ var walkState = await makeWalkStateAsync(params);
2516
2868
  var trees = nodes.map(function(n) { return extractDesignTree(n, 0, maxDepth, detailLevel, filterInvisible, tokenCollector, instanceCollector, walkState); });
2517
2869
  // mainComponent is async-only under documentAccess: dynamic-page — resolve
2518
2870
  // every collected INSTANCE after the (synchronous) tree walk.
@@ -2553,7 +2905,7 @@ handlers.get_design = async function(params) {
2553
2905
  try {
2554
2906
  var tokenCollector = (detailLevel !== "minimal") ? { colors: new Set(), fonts: new Set(), sizes: new Set() } : null;
2555
2907
  var instanceCollector = (detailLevel !== "minimal") ? [] : null;
2556
- var walkState = makeWalkState(p);
2908
+ var walkState = await makeWalkStateAsync(p);
2557
2909
  var tree = extractDesignTree(root, 0, maxDepth, detailLevel, filterInvisible, tokenCollector, instanceCollector, walkState);
2558
2910
  var instanceInfo = await resolveInstanceComponents(instanceCollector);
2559
2911
 
@@ -3129,23 +3481,26 @@ async function exportNodeSvg(node) {
3129
3481
 
3130
3482
  // export_svg — export node as SVG string
3131
3483
  handlers.export_svg = async function(params) {
3132
- var id = params ? params.id : null;
3133
- var nodeName = params ? params.name : null;
3484
+ var id = params ? (params.id || params.nodeId) : null;
3485
+ var nodeName = params ? (params.name || params.nodeName) : null;
3134
3486
  var node = null;
3135
3487
  if (id) node = await findNodeByIdAsync(id);
3136
3488
  if (!node && nodeName) {
3137
3489
  node = findNodeByName(nodeName);
3138
3490
  }
3491
+ if (!node && figma.currentPage.selection.length > 0) {
3492
+ node = figma.currentPage.selection[0];
3493
+ }
3139
3494
  if (!node) node = figma.currentPage;
3140
- if (!node) throw new Error("Node not found");
3495
+ if (!node) throw new Error("Node not found for export");
3141
3496
  var svg = await exportNodeSvg(node);
3142
3497
  return { svg: svg, nodeId: node.id, width: Math.round(node.width), height: Math.round(node.height) };
3143
3498
  };
3144
3499
 
3145
3500
  // export_image — export node as base64 PNG/JPG (for saving to disk, not for inline display)
3146
3501
  handlers.export_image = async function(params) {
3147
- var id = params ? params.id : null;
3148
- var nodeName = params ? params.name : null;
3502
+ var id = params ? (params.id || params.nodeId) : null;
3503
+ var nodeName = params ? (params.name || params.nodeName) : null;
3149
3504
  var format = (params && params.format) ? params.format.toUpperCase() : "PNG";
3150
3505
  var scale = (params && params.scale) ? params.scale : 2;
3151
3506
 
@@ -3156,6 +3511,16 @@ handlers.export_image = async function(params) {
3156
3511
  if (!node && nodeName) {
3157
3512
  node = findNodeByName(nodeName);
3158
3513
  }
3514
+ if (!node && figma.currentPage.selection.length > 0) {
3515
+ node = figma.currentPage.selection[0];
3516
+ }
3517
+ if (!node) {
3518
+ var children = figma.currentPage.children || [];
3519
+ var exportableTypes = ["FRAME", "COMPONENT", "COMPONENT_SET", "SECTION", "INSTANCE", "GROUP"];
3520
+ for (var i = 0; i < children.length; i++) {
3521
+ if (exportableTypes.indexOf(children[i].type) !== -1) { node = children[i]; break; }
3522
+ }
3523
+ }
3159
3524
  if (!node) throw new Error("Node not found for export");
3160
3525
 
3161
3526
  // BUG-05 fix: same as screenshot — force render before export, then put the
@@ -3287,19 +3652,54 @@ handlers.index_scan = async function() {
3287
3652
  };
3288
3653
  };
3289
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
+
3290
3670
 
3291
3671
  // ── plugin-src/handlers-read-detail.js ──
3292
3672
  // ─── READ DETAIL HANDLERS ─────────────────────────────────────────────────────
3293
3673
 
3294
3674
  // get_node_detail — CSS-like properties for a single node (no tree traversal)
3295
3675
  handlers.get_node_detail = async function(params) {
3296
- // Accept id, nodeId, name, nodeName — try ID first then name fallback
3297
- var id = params ? (params.id || params.nodeId) : null;
3298
- 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;
3299
3694
  var node = null;
3300
3695
  if (id) node = await findNodeByIdAsync(id);
3301
3696
  if (!node && nodeName) node = findNodeByName(nodeName);
3302
- 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
+ }
3303
3703
 
3304
3704
  var detail = {
3305
3705
  id: node.id, name: node.name, type: node.type,
@@ -3531,6 +3931,25 @@ handlers.get_node_detail = async function(params) {
3531
3931
  }
3532
3932
  } catch(e) {}
3533
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
+
3534
3953
  // P4: Resolve style IDs → name + values (not just opaque IDs)
3535
3954
  try {
3536
3955
  if (node.textStyleId && typeof node.textStyleId === "string") {
@@ -3557,12 +3976,15 @@ handlers.get_node_detail = async function(params) {
3557
3976
  try { if (node.strokeStyleId && typeof node.strokeStyleId === "string") detail.strokeStyleId = node.strokeStyleId; } catch(e) {}
3558
3977
  try { if (node.effectStyleId && typeof node.effectStyleId === "string") detail.effectStyleId = node.effectStyleId; } catch(e) {}
3559
3978
 
3560
- // Instance: source component reference + property values + overrides + P5 variantLabel
3979
+ // Instance: source component reference + property values + overrides + P5 variantLabel + clean variant & props
3561
3980
  if (node.type === "INSTANCE") {
3562
- // mainComponent is async-only under documentAccess: dynamic-page.
3563
- // describeMainComponent yields componentId/componentName/componentSetName/variantLabel.
3564
3981
  var instComp = await getMainComponentSafe(node);
3565
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
+ }
3566
3988
  // P3: Full override list instead of just count
3567
3989
  try {
3568
3990
  if (node.overrides && node.overrides.length) {
@@ -3588,6 +4010,10 @@ handlers.get_node_detail = async function(params) {
3588
4010
  } catch(e) {}
3589
4011
  }
3590
4012
 
4013
+ // Semantic Role
4014
+ var semanticRole = typeof inferSemanticRole === "function" ? inferSemanticRole(node, detail) : null;
4015
+ if (semanticRole) detail.role = semanticRole;
4016
+
3591
4017
  // Children count + text content summary
3592
4018
  if (node && typeof node === "object" && "children" in node && Array.isArray(node.children)) {
3593
4019
  detail.childCount = node.children.length;
@@ -3695,8 +4121,8 @@ handlers.get_css = async function(params) {
3695
4121
  // Returns: flex layout semantics, token-resolved colors, typography, component instances
3696
4122
  // Much more code-ready than get_design (raw tree) or get_css (single string)
3697
4123
  handlers.get_design_context = async function(params) {
3698
- var id = params ? (params.id || params.nodeId) : null;
3699
- 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;
3700
4126
  var node = null;
3701
4127
  if (id) node = await findNodeByIdAsync(id);
3702
4128
  if (!node && nodeName) node = findNodeByName(nodeName);
@@ -3705,7 +4131,12 @@ handlers.get_design_context = async function(params) {
3705
4131
  var sel = figma.currentPage.selection;
3706
4132
  node = sel && sel.length > 0 ? sel[0] : null;
3707
4133
  }
3708
- 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
+ }
3709
4140
 
3710
4141
  // Build variable name lookup: variableId → name
3711
4142
  // Use getLocalVariablesAsync (single call) instead of per-ID fetches — avoids O(n²) await loop
@@ -3770,6 +4201,26 @@ handlers.get_design_context = async function(params) {
3770
4201
  var fillVal = resolveFill(nd);
3771
4202
  if (fillVal) ctx.fill = fillVal;
3772
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
+
3773
4224
  // Stroke
3774
4225
  try {
3775
4226
  if (nd.strokes && nd.strokes.length && nd.strokes[0].type === "SOLID") {
@@ -3822,16 +4273,17 @@ handlers.get_design_context = async function(params) {
3822
4273
  // documentAccess: dynamic-page, so collect now and resolve after the walk.
3823
4274
  if (nd.type === "INSTANCE") {
3824
4275
  ctxInstances.push({ info: ctx, node: nd });
3825
- try {
3826
- if (nd.componentProperties) {
3827
- var props = {};
3828
- var pks = Object.keys(nd.componentProperties);
3829
- for (var pi = 0; pi < pks.length; pi++) props[pks[pi]] = nd.componentProperties[pks[pi]].value;
3830
- if (pks.length) ctx.component = { properties: props };
3831
- }
3832
- } 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
+ }
3833
4281
  }
3834
4282
 
4283
+ // Role
4284
+ var sRole = typeof inferSemanticRole === "function" ? inferSemanticRole(nd, ctx) : null;
4285
+ if (sRole) ctx.role = sRole;
4286
+
3835
4287
  // Children (limited depth to avoid token overflow)
3836
4288
  if (depth < 4 && nd.children && nd.children.length) {
3837
4289
  ctx.children = [];
@@ -3888,9 +4340,23 @@ handlers.get_design_context = async function(params) {
3888
4340
 
3889
4341
  // Aliases for developer convenience & MCP tool naming compatibility
3890
4342
  handlers.inspect_node = handlers.get_design_context;
4343
+ handlers.inspectNode = handlers.get_design_context;
3891
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;
3892
4353
  handlers.get_node_info = handlers.get_node_detail;
4354
+ handlers.getNodeInfo = handlers.get_node_detail;
3893
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
+
3894
4360
 
3895
4361
  // get_component_map — list every component instance in a frame with variant properties
3896
4362
  // Use this to map Figma components to their code equivalents
@@ -4200,8 +4666,22 @@ handlers.get_variables = async function() {
4200
4666
  return { collections: collections, resolvedTokens: resolvedTokensMap };
4201
4667
  };
4202
4668
 
4203
- handlers.get_tokens = handlers.get_variable_tokens;
4204
- handlers.tokens = handlers.get_variable_tokens;
4669
+ handlers.get_variable_tokens = handlers.get_variables;
4670
+ handlers.getVariableTokens = handlers.get_variables;
4671
+ handlers.get_tokens = handlers.get_variables;
4672
+ handlers.getTokens = handlers.get_variables;
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
+
4205
4685
 
4206
4686
  // ─── EXPORT ASSETS (Batch Icon & Image Extractor) ────────────────────────────
4207
4687
  handlers.export_assets = async function(params) {
@@ -4328,6 +4808,9 @@ handlers.export_assets = async function(params) {
4328
4808
  };
4329
4809
  };
4330
4810
 
4811
+ handlers.exportAssets = handlers.export_assets;
4812
+
4813
+
4331
4814
 
4332
4815
  // ── plugin-src/handlers-tokens.js ──
4333
4816
  // ─── DESIGN TOKEN OPERATIONS (v1.7.0) ───────────────────────────────────────
@@ -5451,6 +5934,29 @@ handlers.removeComponentProperty = async function(params) {
5451
5934
  };
5452
5935
  };
5453
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
+
5454
5960
 
5455
5961
  // ── plugin-src/handlers-write-ops.js ──
5456
5962
  // ─── NEW WRITE OPERATIONS ────────────────────────────────────────────────────
@@ -5548,6 +6054,8 @@ handlers.set_selection = async function(params) {
5548
6054
  return { selected: nodes.map(nodeToInfo) };
5549
6055
  };
5550
6056
 
6057
+ handlers.setSelection = handlers.set_selection;
6058
+
5551
6059
  // batch — execute multiple operations in one call
5552
6060
  // Supported operations: create, modify, delete (single or batch ids), append, clone
5553
6061
  handlers.batch = async function(params) {
@@ -5559,7 +6067,7 @@ handlers.batch = async function(params) {
5559
6067
  var results = [];
5560
6068
  for (var i = 0; i < operations.length; i++) {
5561
6069
  var op = operations[i];
5562
- var handler = handlers[op.operation];
6070
+ var handler = typeof resolveOperationHandler === "function" ? resolveOperationHandler(op.operation) : handlers[op.operation];
5563
6071
  if (!handler) {
5564
6072
  results.push({ index: i, operation: op.operation, success: false, error: "Unknown operation" });
5565
6073
  continue;
@@ -5575,6 +6083,7 @@ handlers.batch = async function(params) {
5575
6083
  };
5576
6084
 
5577
6085
 
6086
+
5578
6087
  // ── plugin-src/main.js ──
5579
6088
  // ─── PLUGIN ENTRY POINT ───────────────────────────────────────────────────────
5580
6089
 
@@ -5901,7 +6410,9 @@ figma.ui.onmessage = async (request) => {
5901
6410
  }
5902
6411
 
5903
6412
  const { id, operation, params } = request;
5904
- const handler = handlers[operation];
6413
+ const handler = typeof resolveOperationHandler === "function"
6414
+ ? resolveOperationHandler(operation)
6415
+ : handlers[operation];
5905
6416
 
5906
6417
  if (!handler) {
5907
6418
  figma.ui.postMessage({
package/plugin/ui.html CHANGED
@@ -457,6 +457,18 @@
457
457
  }
458
458
  .t-tab:hover { color: var(--text-secondary); background: rgba(255,255,255,0.04); }
459
459
  .t-tab.active { color: var(--text-primary); background: var(--surface-card); box-shadow: 0 1px 2px rgba(0,0,0,0.3); }
460
+ .t-tab.tab-err.active { color: var(--ghost-rose); }
461
+ .tab-badge-err {
462
+ font-size: 7.5px;
463
+ font-weight: 700;
464
+ color: var(--ghost-rose);
465
+ background: rgba(251, 113, 133, 0.18);
466
+ border-radius: var(--radius-full);
467
+ padding: 0 4px;
468
+ margin-left: 2px;
469
+ line-height: 1.3;
470
+ display: inline-block;
471
+ }
460
472
 
461
473
  .terminal-meta { display: flex; align-items: center; gap: 6px; }
462
474
 
@@ -810,9 +822,16 @@
810
822
  <button class="t-tab active" onclick="setLogFilter('all', this)">All</button>
811
823
  <button class="t-tab" onclick="setLogFilter('writes', this)">Writes</button>
812
824
  <button class="t-tab" onclick="setLogFilter('reads', this)">Reads</button>
825
+ <button class="t-tab tab-err" id="tab-errors" onclick="setLogFilter('errors', this)">Errors <span class="tab-badge-err" id="tab-err-badge" style="display:none">0</span></button>
813
826
  </div>
814
827
  <div class="terminal-meta">
815
828
  <span class="event-badge" id="log-count">0 ev</span>
829
+ <button class="act-btn icon-only" onclick="copyLogs()" title="Copy console logs (respects active tab)" style="height:18px;width:18px;border-radius:3px;">
830
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
831
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
832
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
833
+ </svg>
834
+ </button>
816
835
  <button class="act-btn icon-only" onclick="clearLog()" title="Clear stream console" style="height:18px;width:18px;border-radius:3px;">
817
836
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
818
837
  <polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/>
@@ -933,15 +952,35 @@
933
952
  if (el) el.classList.add("active");
934
953
 
935
954
  var lines = logEl.querySelectorAll(".log-line");
955
+ var visibleCount = 0;
936
956
  lines.forEach(function(line) {
957
+ var match = false;
937
958
  if (filter === 'all') {
938
- line.style.display = "flex";
959
+ match = true;
939
960
  } else if (filter === 'writes') {
940
- line.style.display = line.dataset.type === 'write' ? "flex" : "none";
961
+ match = line.dataset.type === 'write';
941
962
  } else if (filter === 'reads') {
942
- line.style.display = line.dataset.type === 'read' ? "flex" : "none";
963
+ match = line.dataset.type === 'read';
964
+ } else if (filter === 'errors') {
965
+ match = line.dataset.cls === 'err';
943
966
  }
967
+ line.style.display = match ? "flex" : "none";
968
+ if (match) visibleCount++;
944
969
  });
970
+
971
+ if (logEmptyEl) {
972
+ if (visibleCount === 0 && totalLogEvents > 0) {
973
+ logEmptyEl.style.display = "flex";
974
+ var emptyText = logEmptyEl.querySelector(".terminal-empty-text");
975
+ if (emptyText) emptyText.textContent = filter === 'errors' ? "No errors logged" : "No matching logs";
976
+ } else if (totalLogEvents === 0) {
977
+ logEmptyEl.style.display = "flex";
978
+ var emptyText = logEmptyEl.querySelector(".terminal-empty-text");
979
+ if (emptyText) emptyText.textContent = "Listening on MCP bridge…";
980
+ } else {
981
+ logEmptyEl.style.display = "none";
982
+ }
983
+ }
945
984
  }
946
985
 
947
986
  async function fetchServerStats() {
@@ -1224,8 +1263,76 @@
1224
1263
  }, 500);
1225
1264
  }
1226
1265
 
1266
+ var totalErrors = 0;
1267
+
1268
+ function updateErrorBadge() {
1269
+ var badge = document.getElementById("tab-err-badge");
1270
+ if (!badge) return;
1271
+ if (totalErrors > 0) {
1272
+ badge.textContent = totalErrors;
1273
+ badge.style.display = "inline-block";
1274
+ } else {
1275
+ badge.style.display = "none";
1276
+ }
1277
+ }
1278
+
1279
+ function copyTextToClipboard(text, successMsg) {
1280
+ if (!text) return;
1281
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1282
+ navigator.clipboard.writeText(text).then(function() {
1283
+ showToast(successMsg || "Copied!", "✓");
1284
+ }).catch(function() {
1285
+ fallbackCopy(text, successMsg);
1286
+ });
1287
+ } else {
1288
+ fallbackCopy(text, successMsg);
1289
+ }
1290
+ }
1291
+
1292
+ function fallbackCopy(text, successMsg) {
1293
+ try {
1294
+ var ta = document.createElement("textarea");
1295
+ ta.value = text;
1296
+ ta.style.position = "fixed";
1297
+ ta.style.left = "-9999px";
1298
+ document.body.appendChild(ta);
1299
+ ta.select();
1300
+ document.execCommand("copy");
1301
+ document.body.removeChild(ta);
1302
+ showToast(successMsg || "Copied!", "✓");
1303
+ } catch(e) {
1304
+ showToast("Copy failed", "✗");
1305
+ }
1306
+ }
1307
+
1308
+ function copyLogs() {
1309
+ var lines = logEl.querySelectorAll(".log-line");
1310
+ var visibleTexts = [];
1311
+ lines.forEach(function(line) {
1312
+ if (line.style.display !== "none") {
1313
+ var ts = line.querySelector(".log-ts");
1314
+ var msg = line.querySelector(".log-msg");
1315
+ var text = (ts ? ts.textContent + " " : "") + (msg ? msg.textContent : line.textContent);
1316
+ visibleTexts.push(text.trim());
1317
+ }
1318
+ });
1319
+
1320
+ if (visibleTexts.length === 0) {
1321
+ showToast(activeLogFilter === 'errors' ? "No errors to copy" : "No logs to copy", "ℹ");
1322
+ return;
1323
+ }
1324
+
1325
+ var fullText = visibleTexts.join("\n");
1326
+ var label = activeLogFilter === 'errors' ? "Errors copied (" + visibleTexts.length + ")!" : "Logs copied (" + visibleTexts.length + ")!";
1327
+ copyTextToClipboard(fullText, label);
1328
+ }
1329
+
1227
1330
  function log(msg, cls, opType) {
1228
1331
  totalLogEvents++;
1332
+ if (cls === 'err') {
1333
+ totalErrors++;
1334
+ updateErrorBadge();
1335
+ }
1229
1336
  if (logCountEl) logCountEl.textContent = totalLogEvents + " ev";
1230
1337
 
1231
1338
  if (logEmptyEl) logEmptyEl.style.display = "none";
@@ -1235,6 +1342,9 @@
1235
1342
  var line = document.createElement("div");
1236
1343
  line.className = "log-line";
1237
1344
  if (opType) line.dataset.type = opType;
1345
+ if (cls) line.dataset.cls = cls;
1346
+ line.title = "Click to copy";
1347
+ line.style.cursor = "pointer";
1238
1348
 
1239
1349
  var ts = document.createElement("span");
1240
1350
  ts.className = "log-ts";
@@ -1252,10 +1362,17 @@
1252
1362
  line.appendChild(prompt);
1253
1363
  line.appendChild(content);
1254
1364
 
1365
+ line.addEventListener("click", function() {
1366
+ var text = (ts ? ts.textContent + " " : "") + (content ? content.textContent : line.textContent);
1367
+ copyTextToClipboard(text.trim(), "Copied line!");
1368
+ });
1369
+
1255
1370
  if (activeLogFilter === 'writes' && opType !== 'write') {
1256
1371
  line.style.display = "none";
1257
1372
  } else if (activeLogFilter === 'reads' && opType !== 'read') {
1258
1373
  line.style.display = "none";
1374
+ } else if (activeLogFilter === 'errors' && cls !== 'err') {
1375
+ line.style.display = "none";
1259
1376
  }
1260
1377
 
1261
1378
  logEl.appendChild(line);
@@ -1273,6 +1390,8 @@
1273
1390
  function clearLog() {
1274
1391
  logEl.innerHTML = "";
1275
1392
  totalLogEvents = 0;
1393
+ totalErrors = 0;
1394
+ updateErrorBadge();
1276
1395
  if (logCountEl) logCountEl.textContent = "0 ev";
1277
1396
  var empty = document.createElement("div");
1278
1397
  empty.id = "log-empty";