figma-rust-mcp 2.8.5 → 2.9.6

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/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-26T04:26:26.667Z
4
+ // Generated: 2026-08-27T07:20:35.052Z
5
5
 
6
6
  // ── plugin-src/utils.js ──
7
7
  // ─── UTILS ────────────────────────────────────────────────────────────────────
@@ -328,6 +328,13 @@ function nodeToInfo(node) {
328
328
  return info;
329
329
  }
330
330
 
331
+ // Yield execution back to Figma main thread loop so UI never freezes/blocks
332
+ function yieldToUI(delayMs) {
333
+ return new Promise(function(resolve) {
334
+ setTimeout(resolve, delayMs !== undefined ? delayMs : 0);
335
+ });
336
+ }
337
+
331
338
 
332
339
  // ── plugin-src/svg-path-helpers.js ──
333
340
  // ─── SVG PATH NORMALIZATION ───────────────────────────────────────────────────
@@ -1238,7 +1245,7 @@ const handlers = {};
1238
1245
 
1239
1246
  handlers.status = async () => ({
1240
1247
  connected: true,
1241
- version: "2.8.3",
1248
+ version: "2.9.6",
1242
1249
  fileName: figma.root.name,
1243
1250
  currentPage: figma.currentPage.name,
1244
1251
  pageCount: figma.root.children.length,
@@ -2902,8 +2909,37 @@ handlers.screenshot = async function(params) {
2902
2909
  return Promise.reject(new Error("[v1.9.1-export] " + exportErr.message + " type=" + node.type + " id=" + node.id));
2903
2910
  }
2904
2911
 
2912
+ var annotations = [];
2913
+ if (params && (params.withAnnotations || params.annotated) && "children" in node && Array.isArray(node.children)) {
2914
+ var annCount = 0;
2915
+ for (var ci = 0; ci < node.children.length; ci++) {
2916
+ var child = node.children[ci];
2917
+ if (child.visible === false) continue;
2918
+ annCount++;
2919
+ annotations.push({
2920
+ index: annCount,
2921
+ id: child.id,
2922
+ name: child.name,
2923
+ type: child.type,
2924
+ x: Math.round(child.x),
2925
+ y: Math.round(child.y),
2926
+ width: Math.round(child.width),
2927
+ height: Math.round(child.height)
2928
+ });
2929
+ if (annCount >= 40) break;
2930
+ }
2931
+ }
2932
+
2905
2933
  try {
2906
- return { dataUrl: "data:image/png;base64," + uint8ArrayToBase64(bytes), nodeId: node.id, width: node.width, height: node.height };
2934
+ var resObj = {
2935
+ dataUrl: "data:image/png;base64," + uint8ArrayToBase64(bytes),
2936
+ nodeId: node.id,
2937
+ nodeName: node.name,
2938
+ width: node.width,
2939
+ height: node.height
2940
+ };
2941
+ if (annotations.length) resObj.annotations = annotations;
2942
+ return resObj;
2907
2943
  } catch(encodeErr) {
2908
2944
  return Promise.reject(new Error("[v1.9.1-encode] " + encodeErr.message));
2909
2945
  }
@@ -3006,64 +3042,106 @@ handlers.export_image = async function(params) {
3006
3042
  };
3007
3043
  };
3008
3044
 
3009
- // index_scan — aggregate page nodes, styles, variables, and components in one call for pre-indexing (concurrently)
3045
+ // index_scan — aggregate page nodes, styles, variables, and components in one call for pre-indexing
3010
3046
  handlers.index_scan = async function() {
3011
- var pageNodesPromise = (async function() {
3047
+ if (figma.ui) {
3012
3048
  try {
3013
- var page = figma.currentPage;
3014
- var topFrames = page.children || [];
3015
- var collected = [];
3016
- var CHUNK_SIZE = 10;
3017
-
3018
- for (var i = 0; i < topFrames.length; i += CHUNK_SIZE) {
3019
- var chunk = topFrames.slice(i, i + CHUNK_SIZE);
3020
- var chunkData = chunk.map(function(n) {
3021
- return Object.assign(nodeToInfo(n), { childCount: "children" in n ? n.children.length : 0 });
3022
- });
3023
- collected = collected.concat(chunkData);
3049
+ figma.ui.postMessage({ type: "index-progress", stage: "nodes", percent: 5, label: "Scanning canvas layers…" });
3050
+ } catch(e) {}
3051
+ }
3024
3052
 
3025
- // Stream chunk progressively if there are multiple chunks
3026
- if (topFrames.length > CHUNK_SIZE && figma.ui) {
3027
- try {
3028
- figma.ui.postMessage({ type: "index-chunk", nodes: chunkData });
3029
- } catch(e3) {}
3030
- }
3053
+ var pageNodes = [];
3054
+ try {
3055
+ var page = figma.currentPage;
3056
+ var topFrames = page.children || [];
3057
+ var CHUNK_SIZE = 10;
3058
+ var totalFrames = topFrames.length;
3059
+
3060
+ for (var i = 0; i < topFrames.length; i += CHUNK_SIZE) {
3061
+ var chunk = topFrames.slice(i, i + CHUNK_SIZE);
3062
+ var chunkData = chunk.map(function(n) {
3063
+ return Object.assign(nodeToInfo(n), { childCount: "children" in n ? n.children.length : 0 });
3064
+ });
3065
+ pageNodes = pageNodes.concat(chunkData);
3066
+
3067
+ var nodePct = totalFrames > 0 ? Math.min(40, Math.round(5 + (pageNodes.length / totalFrames) * 35)) : 35;
3068
+ if (figma.ui) {
3069
+ try {
3070
+ figma.ui.postMessage({
3071
+ type: "index-progress",
3072
+ stage: "nodes",
3073
+ percent: nodePct,
3074
+ label: "Layers: " + pageNodes.length + (totalFrames ? "/" + totalFrames : "")
3075
+ });
3076
+ } catch(e) {}
3077
+ }
3078
+
3079
+ // Stream chunk progressively if there are multiple chunks
3080
+ if (topFrames.length > CHUNK_SIZE && figma.ui) {
3081
+ try {
3082
+ figma.ui.postMessage({ type: "index-chunk", nodes: chunkData });
3083
+ } catch(e3) {}
3031
3084
  }
3032
- return collected;
3033
- } catch (e) {}
3034
- return [];
3035
- })();
3036
3085
 
3037
- var stylesPromise = (async function() {
3086
+ // Yield after every chunk so canvas/UI remains completely smooth
3087
+ if (typeof yieldToUI === "function") {
3088
+ await yieldToUI(0);
3089
+ }
3090
+ }
3091
+ } catch (e) {}
3092
+
3093
+ if (typeof yieldToUI === "function") await yieldToUI(0);
3094
+
3095
+ if (figma.ui) {
3038
3096
  try {
3039
- if (handlers.get_styles) return await handlers.get_styles({});
3040
- } catch (e) {}
3041
- return null;
3042
- })();
3097
+ figma.ui.postMessage({ type: "index-progress", stage: "styles", percent: 45, label: "Indexing color & text styles…" });
3098
+ } catch(e) {}
3099
+ }
3100
+
3101
+ var styles = null;
3102
+ try {
3103
+ if (handlers.get_styles) styles = await handlers.get_styles({});
3104
+ } catch (e) {}
3105
+
3106
+ if (typeof yieldToUI === "function") await yieldToUI(0);
3043
3107
 
3044
- var varsPromise = (async function() {
3108
+ if (figma.ui) {
3045
3109
  try {
3046
- if (handlers.get_variables) return await handlers.get_variables({});
3047
- } catch (e) {}
3048
- return null;
3049
- })();
3110
+ figma.ui.postMessage({ type: "index-progress", stage: "variables", percent: 65, label: "Indexing variables & tokens…" });
3111
+ } catch(e) {}
3112
+ }
3113
+
3114
+ var variables = null;
3115
+ try {
3116
+ if (handlers.get_variables) variables = await handlers.get_variables({});
3117
+ } catch (e) {}
3050
3118
 
3051
- var compsPromise = (async function() {
3119
+ if (typeof yieldToUI === "function") await yieldToUI(0);
3120
+
3121
+ if (figma.ui) {
3052
3122
  try {
3053
- if (handlers.get_local_components) return await handlers.get_local_components({});
3054
- } catch (e) {}
3055
- return null;
3056
- })();
3123
+ figma.ui.postMessage({ type: "index-progress", stage: "components", percent: 85, label: "Indexing components & sets…" });
3124
+ } catch(e) {}
3125
+ }
3057
3126
 
3058
- var results = await Promise.all([pageNodesPromise, stylesPromise, varsPromise, compsPromise]);
3059
- var pageNodes = results[0];
3060
- var styles = results[1];
3061
- var variables = results[2];
3062
- var components = results[3];
3127
+ var components = null;
3128
+ try {
3129
+ if (handlers.get_local_components) components = await handlers.get_local_components({});
3130
+ } catch (e) {}
3131
+
3132
+ if (figma.ui) {
3133
+ try {
3134
+ figma.ui.postMessage({ type: "index-progress", stage: "done", percent: 100, label: "Index ready" });
3135
+ } catch(e) {}
3136
+ }
3137
+
3138
+ var resolvedSid = figma.fileKey || (function() {
3139
+ try { return figma.root.getPluginData("mcp_session_id") || "_default"; } catch(e) { return "_default"; }
3140
+ })();
3063
3141
 
3064
3142
  return {
3065
3143
  fileName: figma.root ? figma.root.name : "unknown",
3066
- sessionId: figma.root ? figma.root.id : "_default",
3144
+ sessionId: resolvedSid,
3067
3145
  pageNodes: pageNodes,
3068
3146
  styles: styles,
3069
3147
  variables: variables,
@@ -3749,46 +3827,66 @@ handlers.get_styles = async function() {
3749
3827
 
3750
3828
  // get_local_components — enhanced component listing with descriptions and properties
3751
3829
  handlers.get_local_components = async function() {
3752
- await figma.loadAllPagesAsync();
3830
+ if (typeof figma.loadAllPagesAsync === "function") {
3831
+ try { await figma.loadAllPagesAsync(); } catch (e) {}
3832
+ }
3753
3833
  var comps = figma.root.findAllWithCriteria({ types: ["COMPONENT"] });
3834
+ if (typeof yieldToUI === "function") await yieldToUI(0);
3754
3835
  var sets = figma.root.findAllWithCriteria({ types: ["COMPONENT_SET"] });
3755
-
3756
- return {
3757
- components: comps.map(function(c) {
3758
- var info = {
3759
- id: c.id, name: c.name, key: c.key || null,
3760
- description: c.description || "",
3761
- setName: (c.parent && c.parent.type === "COMPONENT_SET") ? c.parent.name : null,
3762
- width: Math.round(c.width), height: Math.round(c.height),
3763
- page: c.parent ? (function findPage(n) {
3764
- while (n && n.type !== "PAGE") n = n.parent;
3765
- return n ? n.name : null;
3766
- })(c) : null,
3767
- };
3768
- // Component properties (variant props)
3769
- try {
3770
- if (c.componentPropertyDefinitions) {
3771
- var defs = c.componentPropertyDefinitions;
3772
- var props = {};
3773
- for (var key in defs) {
3774
- if (Object.prototype.hasOwnProperty.call(defs, key)) {
3775
- props[key] = { type: defs[key].type, defaultValue: defs[key].defaultValue };
3776
- if (defs[key].variantOptions) props[key].options = defs[key].variantOptions;
3777
- }
3836
+ if (typeof yieldToUI === "function") await yieldToUI(0);
3837
+
3838
+ var componentList = [];
3839
+ var COMP_BATCH_SIZE = 50;
3840
+ for (var ci = 0; ci < comps.length; ci++) {
3841
+ var c = comps[ci];
3842
+ var info = {
3843
+ id: c.id, name: c.name, key: c.key || null,
3844
+ description: c.description || "",
3845
+ setName: (c.parent && c.parent.type === "COMPONENT_SET") ? c.parent.name : null,
3846
+ width: Math.round(c.width), height: Math.round(c.height),
3847
+ page: c.parent ? (function findPage(n) {
3848
+ while (n && n.type !== "PAGE") n = n.parent;
3849
+ return n ? n.name : null;
3850
+ })(c) : null,
3851
+ };
3852
+ // Component properties (variant props)
3853
+ try {
3854
+ if (c.componentPropertyDefinitions) {
3855
+ var defs = c.componentPropertyDefinitions;
3856
+ var props = {};
3857
+ for (var key in defs) {
3858
+ if (Object.prototype.hasOwnProperty.call(defs, key)) {
3859
+ props[key] = { type: defs[key].type, defaultValue: defs[key].defaultValue };
3860
+ if (defs[key].variantOptions) props[key].options = defs[key].variantOptions;
3778
3861
  }
3779
- info.properties = props;
3780
3862
  }
3781
- } catch(e) { /* skip properties */ }
3782
- return info;
3783
- }),
3784
- componentSets: sets.map(function(s) {
3785
- return {
3786
- id: s.id, name: s.name, key: s.key || null,
3787
- description: s.description || "",
3788
- variantCount: s.children ? s.children.length : 0,
3789
- };
3790
- }),
3791
- total: comps.length + sets.length,
3863
+ info.properties = props;
3864
+ }
3865
+ } catch(e) { /* skip properties */ }
3866
+ componentList.push(info);
3867
+
3868
+ if (ci > 0 && ci % COMP_BATCH_SIZE === 0 && typeof yieldToUI === "function") {
3869
+ await yieldToUI(0);
3870
+ }
3871
+ }
3872
+
3873
+ var componentSets = [];
3874
+ for (var si = 0; si < sets.length; si++) {
3875
+ var s = sets[si];
3876
+ componentSets.push({
3877
+ id: s.id, name: s.name, key: s.key || null,
3878
+ description: s.description || "",
3879
+ variantCount: s.children ? s.children.length : 0,
3880
+ });
3881
+ if (si > 0 && si % COMP_BATCH_SIZE === 0 && typeof yieldToUI === "function") {
3882
+ await yieldToUI(0);
3883
+ }
3884
+ }
3885
+
3886
+ return {
3887
+ components: componentList,
3888
+ componentSets: componentSets,
3889
+ total: componentList.length + componentSets.length,
3792
3890
  };
3793
3891
  };
3794
3892
 
@@ -3871,12 +3969,17 @@ handlers.get_variables = async function() {
3871
3969
  values: values,
3872
3970
  description: v.description || "",
3873
3971
  });
3972
+
3973
+ if (vi > 0 && vi % 50 === 0 && typeof yieldToUI === "function") {
3974
+ await yieldToUI(0);
3975
+ }
3874
3976
  }
3875
3977
  collections.push({
3876
3978
  id: col.id, name: col.name,
3877
3979
  modes: col.modes.map(function(m) { return { id: m.modeId, name: m.name }; }),
3878
3980
  variables: variables,
3879
3981
  });
3982
+ if (typeof yieldToUI === "function") await yieldToUI(0);
3880
3983
  }
3881
3984
  } catch(e) {
3882
3985
  return { error: "Variables API not available: " + e.message, collections: [] };
@@ -3884,6 +3987,131 @@ handlers.get_variables = async function() {
3884
3987
  return { collections: collections };
3885
3988
  };
3886
3989
 
3990
+ // ─── EXPORT ASSETS (Batch Icon & Image Extractor) ────────────────────────────
3991
+ handlers.export_assets = async function(params) {
3992
+ var id = params ? (params.id || params.nodeId) : null;
3993
+ var targetNode = null;
3994
+ if (id) targetNode = await findNodeByIdAsync(id);
3995
+ if (!targetNode) {
3996
+ var sel = figma.currentPage.selection;
3997
+ targetNode = sel && sel.length > 0 ? sel[0] : figma.currentPage;
3998
+ }
3999
+ if (!targetNode) throw new Error("No node found to export assets from");
4000
+
4001
+ var icons = [];
4002
+ var images = [];
4003
+ var seenNames = {};
4004
+
4005
+ function sanitizeAssetName(rawName, fallback) {
4006
+ var name = (rawName || fallback)
4007
+ .toLowerCase()
4008
+ .replace(/^icon[s\/_\-\s]+/, '')
4009
+ .replace(/^ic[_\-\s]+/, '')
4010
+ .replace(/[\/\s_\.]+/g, '-')
4011
+ .replace(/[^a-z0-9\-]/g, '')
4012
+ .replace(/^-+|-+$/g, '');
4013
+ if (!name) name = fallback;
4014
+ if (seenNames[name]) {
4015
+ seenNames[name]++;
4016
+ return name + "-" + seenNames[name];
4017
+ }
4018
+ seenNames[name] = 1;
4019
+ return name;
4020
+ }
4021
+
4022
+ function isVectorIcon(nd) {
4023
+ if (!nd || nd.visible === false) return false;
4024
+ var t = nd.type;
4025
+ if (t === "VECTOR" || t === "BOOLEAN_OPERATION" || t === "STAR" || t === "POLYGON") return true;
4026
+ if ((t === "FRAME" || t === "COMPONENT" || t === "INSTANCE" || t === "GROUP") && nd.width <= 96 && nd.height <= 96) {
4027
+ var n = nd.name.toLowerCase();
4028
+ if (n.indexOf("icon") >= 0 || n.indexOf("ic_") >= 0 || n.indexOf("ic-") >= 0) return true;
4029
+ if (nd.children && nd.children.length > 0 && nd.children.length <= 8) {
4030
+ var allVectors = nd.children.every(function(c) {
4031
+ return c.type === "VECTOR" || c.type === "BOOLEAN_OPERATION" || c.type === "LINE" || c.type === "ELLIPSE" || c.type === "RECTANGLE";
4032
+ });
4033
+ if (allVectors) return true;
4034
+ }
4035
+ }
4036
+ return false;
4037
+ }
4038
+
4039
+ function hasImageFill(nd) {
4040
+ if (!nd || !nd.fills || !Array.isArray(nd.fills)) return false;
4041
+ return nd.fills.some(function(f) { return f && f.type === "IMAGE" && f.visible !== false; });
4042
+ }
4043
+
4044
+ var nodesToInspect = [];
4045
+ function collectAssetNodes(nd, depth) {
4046
+ if (!nd || nd.visible === false || depth > 20) return;
4047
+ if (isVectorIcon(nd)) {
4048
+ nodesToInspect.push({ node: nd, kind: "icon" });
4049
+ return;
4050
+ }
4051
+ if (hasImageFill(nd)) {
4052
+ nodesToInspect.push({ node: nd, kind: "image" });
4053
+ }
4054
+ if (nd.children && Array.isArray(nd.children)) {
4055
+ for (var ci = 0; ci < nd.children.length; ci++) {
4056
+ collectAssetNodes(nd.children[ci], depth + 1);
4057
+ }
4058
+ }
4059
+ }
4060
+
4061
+ if (targetNode === figma.currentPage) {
4062
+ for (var pi = 0; pi < targetNode.children.length; pi++) {
4063
+ collectAssetNodes(targetNode.children[pi], 0);
4064
+ }
4065
+ } else {
4066
+ collectAssetNodes(targetNode, 0);
4067
+ }
4068
+
4069
+ var exportLimit = Math.min(nodesToInspect.length, 60);
4070
+ for (var i = 0; i < exportLimit; i++) {
4071
+ var item = nodesToInspect[i];
4072
+ var nd = item.node;
4073
+ var baseName = sanitizeAssetName(nd.name, "asset-" + (i + 1));
4074
+
4075
+ if (item.kind === "icon") {
4076
+ try {
4077
+ var svgBytes = await nd.exportAsync({ format: "SVG" });
4078
+ var svgStr = String.fromCharCode.apply(null, Array.from(svgBytes));
4079
+ icons.push({
4080
+ id: nd.id,
4081
+ name: baseName,
4082
+ fileName: baseName + ".svg",
4083
+ width: Math.round(nd.width),
4084
+ height: Math.round(nd.height),
4085
+ svg: svgStr
4086
+ });
4087
+ } catch (e) {}
4088
+ } else if (item.kind === "image") {
4089
+ try {
4090
+ var pngBytes = await nd.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: 2 } });
4091
+ var b64 = uint8ArrayToBase64(pngBytes);
4092
+ images.push({
4093
+ id: nd.id,
4094
+ name: baseName,
4095
+ fileName: baseName + ".png",
4096
+ width: Math.round(nd.width),
4097
+ height: Math.round(nd.height),
4098
+ dataUrl: "data:image/png;base64," + b64
4099
+ });
4100
+ } catch (e) {}
4101
+ }
4102
+ }
4103
+
4104
+ return {
4105
+ success: true,
4106
+ sourceNodeId: targetNode.id,
4107
+ sourceNodeName: targetNode.name,
4108
+ totalIcons: icons.length,
4109
+ totalImages: images.length,
4110
+ icons: icons,
4111
+ images: images
4112
+ };
4113
+ };
4114
+
3887
4115
 
3888
4116
  // ── plugin-src/handlers-tokens.js ──
3889
4117
  // ─── DESIGN TOKEN OPERATIONS (v1.7.0) ───────────────────────────────────────
@@ -5148,12 +5376,51 @@ figma.clientStorage.getAsync("mcp_window_size").then(function(saved) {
5148
5376
  }
5149
5377
  }).catch(function() {});
5150
5378
 
5379
+ // ─── UNIQUE SESSION IDENTIFIER (Multi-tab Support) ───────────────────────────
5380
+ // In Figma, figma.root.id is always "0:0" across all files. To support running
5381
+ // multiple plugins concurrently across multiple tabs/files, generate and persist
5382
+ // a unique document session ID in document pluginData.
5383
+ function getSavedFileKey() {
5384
+ if (figma.fileKey) return figma.fileKey;
5385
+ try {
5386
+ var saved = figma.root.getPluginData("figma_file_key");
5387
+ if (saved && saved.length > 0) return saved;
5388
+ } catch(e) {}
5389
+ return null;
5390
+ }
5391
+
5392
+ function getOrCreateSessionId() {
5393
+ var fk = getSavedFileKey();
5394
+ if (fk) {
5395
+ return fk;
5396
+ }
5397
+ try {
5398
+ var stored = figma.root.getPluginData("mcp_session_id");
5399
+ if (stored && stored.length > 0) {
5400
+ return stored;
5401
+ }
5402
+ } catch(e) {}
5403
+
5404
+ var rand = Math.random().toString(36).substring(2, 10);
5405
+ var ts = Date.now().toString(36);
5406
+ var newId = "doc_" + rand + "_" + ts;
5407
+ try {
5408
+ figma.root.setPluginData("mcp_session_id", newId);
5409
+ } catch(e) {}
5410
+ return newId;
5411
+ }
5412
+
5413
+ var currentSessionId = getOrCreateSessionId();
5414
+ var currentFileName = figma.root ? figma.root.name : "Untitled";
5415
+ var currentFileKey = getSavedFileKey();
5416
+
5151
5417
  // Broadcast active file / session metadata on startup
5152
5418
  try {
5153
5419
  figma.ui.postMessage({
5154
5420
  type: "session-info",
5155
- sessionId: figma.root.id,
5156
- fileName: figma.root.name
5421
+ sessionId: currentSessionId,
5422
+ fileName: currentFileName,
5423
+ fileKey: currentFileKey
5157
5424
  });
5158
5425
  } catch (e) {}
5159
5426
 
@@ -5162,13 +5429,33 @@ figma.on("selectionchange", function() {
5162
5429
  try {
5163
5430
  var sel = figma.currentPage.selection;
5164
5431
  var summary = [];
5432
+ var fullNode = null;
5165
5433
  for (var i = 0; i < Math.min(sel.length, 5); i++) {
5166
- summary.push({ id: sel[i].id, name: sel[i].name, type: sel[i].type });
5434
+ var n = sel[i];
5435
+ // If node is an internal instance sub-layer (starts with I...), find its main component instance or top frame
5436
+ var mainId = n.id;
5437
+ if (mainId.startsWith("I") && mainId.indexOf(";") !== -1) {
5438
+ var parts = mainId.split(";");
5439
+ mainId = parts[parts.length - 1]; // Use real canonical node id
5440
+ }
5441
+ summary.push({
5442
+ id: n.id,
5443
+ mainId: mainId,
5444
+ name: n.name,
5445
+ type: n.type,
5446
+ width: typeof n.width === "number" ? Math.round(n.width) : undefined,
5447
+ height: typeof n.height === "number" ? Math.round(n.height) : undefined
5448
+ });
5449
+ }
5450
+ if (sel.length === 1 && typeof nodeToInfo === "function") {
5451
+ try { fullNode = nodeToInfo(sel[0]); } catch(e0) {}
5167
5452
  }
5168
5453
  figma.ui.postMessage({
5169
5454
  type: "selection-change",
5170
5455
  count: sel.length,
5171
- selection: summary
5456
+ pageName: figma.currentPage ? figma.currentPage.name : undefined,
5457
+ selection: summary,
5458
+ fullNode: fullNode
5172
5459
  });
5173
5460
  } catch (e) {}
5174
5461
  });
@@ -5190,6 +5477,28 @@ function onDocChange(event) {
5190
5477
  var ids = Array.from(pendingChangedNodeIds);
5191
5478
  pendingChangedNodeIds.clear();
5192
5479
 
5480
+ if (ids.length === 1) {
5481
+ try {
5482
+ var singleNode = await findNodeByIdAsync(ids[0]);
5483
+ if (singleNode) {
5484
+ figma.ui.postMessage({
5485
+ type: "delta-diff",
5486
+ id: singleNode.id,
5487
+ diff: {
5488
+ name: singleNode.name,
5489
+ characters: "characters" in singleNode ? singleNode.characters : undefined,
5490
+ visible: singleNode.visible,
5491
+ width: singleNode.width,
5492
+ height: singleNode.height,
5493
+ x: singleNode.x,
5494
+ y: singleNode.y
5495
+ }
5496
+ });
5497
+ return;
5498
+ }
5499
+ } catch(e1) {}
5500
+ }
5501
+
5193
5502
  if (ids.length > 0 && ids.length <= 15) {
5194
5503
  // Incremental micro-diff update for fast real-time editing
5195
5504
  var diffNodes = [];
@@ -5209,24 +5518,27 @@ function onDocChange(event) {
5209
5518
 
5210
5519
  // Fallback for large batch changes: mark dirty or trigger background sync
5211
5520
  figma.ui.postMessage({ type: "document-change" });
5212
- }, 800);
5521
+ }, 500);
5213
5522
  } catch (e) {}
5214
5523
  }
5215
5524
 
5216
- // In dynamic-page mode, Figma requires figma.loadAllPagesAsync() before registering documentchange
5217
- if (typeof figma.loadAllPagesAsync === "function") {
5218
- figma.loadAllPagesAsync().then(function() {
5525
+ // In dynamic-page mode, Figma requires figma.loadAllPagesAsync() before registering documentchange.
5526
+ // Delay loading all pages slightly so plugin UI opens and paints immediately without UI freeze.
5527
+ setTimeout(function() {
5528
+ if (typeof figma.loadAllPagesAsync === "function") {
5529
+ figma.loadAllPagesAsync().then(function() {
5530
+ try {
5531
+ figma.on("documentchange", onDocChange);
5532
+ } catch (e) {}
5533
+ }).catch(function() {});
5534
+ } else {
5219
5535
  try {
5220
5536
  figma.on("documentchange", onDocChange);
5221
5537
  } catch (e) {}
5222
- }).catch(function() {});
5223
- } else {
5224
- try {
5225
- figma.on("documentchange", onDocChange);
5226
- } catch (e) {}
5227
- }
5538
+ }
5539
+ }, 800);
5228
5540
 
5229
- // Background initial scan after 1.2s startup delay (ensures UI WebSocket is connected)
5541
+ // Background initial scan after 1.8s startup delay (ensures UI is fully rendered and WebSocket connected)
5230
5542
  setTimeout(async function() {
5231
5543
  try {
5232
5544
  if (handlers.index_scan) {
@@ -5235,13 +5547,13 @@ setTimeout(async function() {
5235
5547
  figma.ui.postMessage({
5236
5548
  type: "index-update",
5237
5549
  data: scanResult,
5238
- fileName: figma.root ? figma.root.name : "unknown",
5239
- sessionId: figma.root ? figma.root.id : "_default",
5550
+ fileName: currentFileName,
5551
+ sessionId: currentSessionId,
5240
5552
  startMs: startMs
5241
5553
  });
5242
5554
  }
5243
5555
  } catch (e) {}
5244
- }, 1200);
5556
+ }, 1800);
5245
5557
 
5246
5558
  // ─── DISPATCHER ───────────────────────────────────────────────────────────────
5247
5559
 
@@ -5288,6 +5600,72 @@ figma.ui.onmessage = async (request) => {
5288
5600
  return;
5289
5601
  }
5290
5602
 
5603
+ // Set and persist Figma File Key
5604
+ if (request.type === "set-file-key") {
5605
+ try {
5606
+ var newFileKey = (request.fileKey || "").trim();
5607
+ if (newFileKey) {
5608
+ figma.root.setPluginData("figma_file_key", newFileKey);
5609
+ currentFileKey = newFileKey;
5610
+ figma.ui.postMessage({
5611
+ type: "session-info",
5612
+ sessionId: currentSessionId,
5613
+ fileName: currentFileName,
5614
+ fileKey: newFileKey
5615
+ });
5616
+ figma.notify("Figma File Key saved!", { timeout: 1500 });
5617
+ }
5618
+ } catch(e) {}
5619
+ return;
5620
+ }
5621
+
5622
+ // Toast notification from UI
5623
+ if (request.type === "notify") {
5624
+ try {
5625
+ if (request.message) {
5626
+ figma.notify(request.message, { timeout: request.timeout || 1500, error: request.error || false });
5627
+ }
5628
+ } catch(e) {}
5629
+ return;
5630
+ }
5631
+
5632
+ // Zoom to selection when clicked on selection bar
5633
+ if (request.type === "zoom-to-selection") {
5634
+ try {
5635
+ if (figma.currentPage.selection.length > 0) {
5636
+ figma.viewport.scrollAndZoomIntoView(figma.currentPage.selection);
5637
+ figma.notify("Focused on selection", { timeout: 1000 });
5638
+ }
5639
+ } catch(e) {}
5640
+ return;
5641
+ }
5642
+
5643
+ // Export selection image as PNG for UI clipboard/download
5644
+ if (request.type === "export-selection-image") {
5645
+ try {
5646
+ var sel = figma.currentPage.selection;
5647
+ if (!sel || sel.length === 0) {
5648
+ figma.notify("Please select a layer to capture", { timeout: 1500 });
5649
+ return;
5650
+ }
5651
+ var targetNode = sel[0];
5652
+ var scale = request.scale || 2;
5653
+ var bytes = await targetNode.exportAsync({
5654
+ format: "PNG",
5655
+ constraint: { type: "SCALE", value: scale }
5656
+ });
5657
+ figma.ui.postMessage({
5658
+ type: "selection-image-exported",
5659
+ nodeId: targetNode.id,
5660
+ nodeName: targetNode.name,
5661
+ bytes: Array.from(bytes)
5662
+ });
5663
+ } catch (err) {
5664
+ figma.notify("Export failed: " + (err && err.message ? err.message : String(err)), { error: true });
5665
+ }
5666
+ return;
5667
+ }
5668
+
5291
5669
  // Handle manual reindex request from UI
5292
5670
  if (request.type === "manual-reindex") {
5293
5671
  try {
@@ -5297,8 +5675,8 @@ figma.ui.onmessage = async (request) => {
5297
5675
  figma.ui.postMessage({
5298
5676
  type: "index-update",
5299
5677
  data: scanResult,
5300
- fileName: figma.root ? figma.root.name : "unknown",
5301
- sessionId: figma.root ? figma.root.id : "_default",
5678
+ fileName: currentFileName,
5679
+ sessionId: currentSessionId,
5302
5680
  startMs: startMs
5303
5681
  });
5304
5682
  }