@biolab/talk-to-figma 0.6.0 → 0.8.0

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.
@@ -263,6 +263,20 @@ async function handleCommand(command, params) {
263
263
  return await createComponent(params);
264
264
  case "create_component_set":
265
265
  return await createComponentSet(params);
266
+ case "set_component_text_property":
267
+ return await setComponentTextProperty(params);
268
+ case "rename_node":
269
+ return await renameNode(params);
270
+ case "rename_nodes":
271
+ return await renameNodes(params);
272
+ case "add_boolean_property":
273
+ return await addBooleanProperty(params);
274
+ case "add_instance_swap_property":
275
+ return await addInstanceSwapProperty(params);
276
+ case "sync_layer_fills":
277
+ return await syncLayerFills(params);
278
+ case "move_child_to_index":
279
+ return await moveChildToIndex(params);
266
280
  default:
267
281
  throw new Error(`Unknown command: ${command}`);
268
282
  }
@@ -1515,20 +1529,42 @@ async function setVariableBinding(params) {
1515
1529
  }
1516
1530
 
1517
1531
  async function createComponentInstance(params) {
1518
- const { componentKey, x = 0, y = 0 } = params || {};
1532
+ const { componentKey, x = 0, y = 0, parentId } = params || {};
1519
1533
 
1520
1534
  if (!componentKey) {
1521
1535
  throw new Error("Missing componentKey parameter");
1522
1536
  }
1523
1537
 
1524
1538
  try {
1525
- const component = await figma.importComponentByKeyAsync(componentKey);
1539
+ var component;
1540
+ // If componentKey contains ":", treat as node ID (local component)
1541
+ if (componentKey.indexOf(":") !== -1) {
1542
+ var localNode = await figma.getNodeByIdAsync(componentKey);
1543
+ if (!localNode || localNode.type !== "COMPONENT") {
1544
+ throw new Error("Local component not found with ID: " + componentKey);
1545
+ }
1546
+ component = localNode;
1547
+ } else {
1548
+ // Try import by key (published library components)
1549
+ component = await figma.importComponentByKeyAsync(componentKey);
1550
+ }
1526
1551
  const instance = component.createInstance();
1527
1552
 
1528
1553
  instance.x = x;
1529
1554
  instance.y = y;
1530
1555
 
1531
- figma.currentPage.appendChild(instance);
1556
+ if (parentId) {
1557
+ const parentNode = await figma.getNodeByIdAsync(parentId);
1558
+ if (!parentNode) {
1559
+ throw new Error(`Parent node not found: ${parentId}`);
1560
+ }
1561
+ if (!("appendChild" in parentNode)) {
1562
+ throw new Error(`Parent node does not support children: ${parentId}`);
1563
+ }
1564
+ parentNode.appendChild(instance);
1565
+ } else {
1566
+ figma.currentPage.appendChild(instance);
1567
+ }
1532
1568
 
1533
1569
  return {
1534
1570
  id: instance.id,
@@ -1646,6 +1682,69 @@ async function createComponentSet(params) {
1646
1682
  }
1647
1683
  }
1648
1684
 
1685
+ async function renameNode(params) {
1686
+ var nodeId = (params || {}).nodeId;
1687
+ var name = (params || {}).name;
1688
+
1689
+ if (!nodeId) {
1690
+ throw new Error("Missing nodeId parameter");
1691
+ }
1692
+ if (!name && name !== "") {
1693
+ throw new Error("Missing name parameter");
1694
+ }
1695
+
1696
+ var node = await figma.getNodeByIdAsync(nodeId);
1697
+ if (!node) {
1698
+ throw new Error("Node not found with ID: " + nodeId);
1699
+ }
1700
+
1701
+ node.name = name;
1702
+
1703
+ return {
1704
+ id: node.id,
1705
+ name: node.name,
1706
+ type: node.type,
1707
+ };
1708
+ }
1709
+
1710
+ async function renameNodes(params) {
1711
+ var nodes = (params || {}).nodes;
1712
+
1713
+ if (!nodes || !Array.isArray(nodes) || nodes.length === 0) {
1714
+ throw new Error("Missing or empty nodes array. Provide [{nodeId, name}].");
1715
+ }
1716
+
1717
+ var results = [];
1718
+ var errors = [];
1719
+
1720
+ for (var i = 0; i < nodes.length; i++) {
1721
+ var item = nodes[i];
1722
+ var nodeId = item.nodeId;
1723
+ var name = item.name;
1724
+
1725
+ if (!nodeId || (!name && name !== "")) {
1726
+ errors.push("Invalid entry at index " + i + ": missing nodeId or name");
1727
+ continue;
1728
+ }
1729
+
1730
+ var node = await figma.getNodeByIdAsync(nodeId);
1731
+ if (!node) {
1732
+ errors.push("Node not found: " + nodeId);
1733
+ continue;
1734
+ }
1735
+
1736
+ node.name = name;
1737
+ results.push({ id: node.id, name: node.name, type: node.type });
1738
+ }
1739
+
1740
+ return {
1741
+ renamed: results.length,
1742
+ errors: errors.length,
1743
+ results: results,
1744
+ errorMessages: errors,
1745
+ };
1746
+ }
1747
+
1649
1748
  async function exportNodeAsImage(params) {
1650
1749
  const { nodeId, scale = 1 } = params || {};
1651
1750
 
@@ -4561,3 +4660,390 @@ async function setSelections(params) {
4561
4660
  message: `Selected ${nodes.length} nodes${notFoundIds.length > 0 ? ` (${notFoundIds.length} not found)` : ''}`
4562
4661
  };
4563
4662
  }
4663
+
4664
+ async function setComponentTextProperty(params) {
4665
+ const { componentId, propertyName, defaultValue, textNodeIds } = params;
4666
+
4667
+ if (!componentId) throw new Error("Missing componentId parameter");
4668
+ if (!propertyName) throw new Error("Missing propertyName parameter");
4669
+ if (!textNodeIds || !Array.isArray(textNodeIds) || textNodeIds.length === 0) {
4670
+ throw new Error("Missing or empty textNodeIds parameter");
4671
+ }
4672
+
4673
+ const componentNode = await figma.getNodeByIdAsync(componentId);
4674
+ if (!componentNode) {
4675
+ throw new Error(`Node not found with ID: ${componentId}`);
4676
+ }
4677
+
4678
+ if (componentNode.type !== "COMPONENT" && componentNode.type !== "COMPONENT_SET") {
4679
+ throw new Error(`Node ${componentId} is type ${componentNode.type}, expected COMPONENT or COMPONENT_SET`);
4680
+ }
4681
+
4682
+ const results = [];
4683
+ const errors = [];
4684
+
4685
+ // Determine the property owner (ComponentSet or standalone Component)
4686
+ var propertyOwner = componentNode;
4687
+
4688
+ // For ComponentSet, property must be added on the set itself, not variants
4689
+ var propDefault = defaultValue || "Button";
4690
+
4691
+ // Check if property already exists on the property owner
4692
+ var existingProps = propertyOwner.componentPropertyDefinitions;
4693
+ var propertyKey = null;
4694
+
4695
+ for (var key in existingProps) {
4696
+ if (existingProps.hasOwnProperty(key)) {
4697
+ var def = existingProps[key];
4698
+ if (def.type === "TEXT" && key.startsWith(propertyName + "#")) {
4699
+ propertyKey = key;
4700
+ break;
4701
+ }
4702
+ }
4703
+ }
4704
+
4705
+ // Add property on the owner if it doesn't exist
4706
+ if (!propertyKey) {
4707
+ propertyKey = propertyOwner.addComponentProperty(propertyName, "TEXT", propDefault);
4708
+ }
4709
+
4710
+ // Get all component nodes that contain the text nodes
4711
+ var components = [];
4712
+ if (componentNode.type === "COMPONENT_SET") {
4713
+ components = componentNode.children.filter(function(c) { return c.type === "COMPONENT"; });
4714
+ } else {
4715
+ components = [componentNode];
4716
+ }
4717
+
4718
+ for (var i = 0; i < components.length; i++) {
4719
+ var comp = components[i];
4720
+ try {
4721
+ // Find text nodes in this component that match the requested IDs
4722
+ for (var j = 0; j < textNodeIds.length; j++) {
4723
+ var textNodeId = textNodeIds[j];
4724
+ var textNode = comp.findOne(function(n) { return n.id === textNodeId; });
4725
+ if (textNode && textNode.type === "TEXT") {
4726
+ // Load font for the text node
4727
+ await figma.loadFontAsync(textNode.fontName);
4728
+
4729
+ var existingRefs = textNode.componentPropertyReferences || {};
4730
+ var newRefs = Object.assign({}, existingRefs);
4731
+ newRefs.characters = propertyKey;
4732
+ textNode.componentPropertyReferences = newRefs;
4733
+ results.push({
4734
+ componentId: comp.id,
4735
+ componentName: comp.name,
4736
+ textNodeId: textNode.id,
4737
+ propertyKey: propertyKey,
4738
+ });
4739
+ }
4740
+ }
4741
+ } catch (err) {
4742
+ errors.push({
4743
+ componentId: comp.id,
4744
+ componentName: comp.name,
4745
+ error: err.message || String(err),
4746
+ });
4747
+ }
4748
+ }
4749
+
4750
+ return {
4751
+ success: true,
4752
+ linked: results.length,
4753
+ errors: errors.length,
4754
+ propertyKey: propertyKey,
4755
+ results: results,
4756
+ errorDetails: errors,
4757
+ message: "Linked " + results.length + " text nodes to \"" + propertyName + "\" property" + (errors.length > 0 ? " (" + errors.length + " errors)" : ""),
4758
+ };
4759
+ }
4760
+
4761
+ async function addBooleanProperty(params) {
4762
+ var componentSetId = (params || {}).componentSetId;
4763
+ var propertyName = (params || {}).propertyName;
4764
+ var defaultValue = (params || {}).defaultValue;
4765
+ var layerName = (params || {}).layerName;
4766
+
4767
+ if (!componentSetId) throw new Error("Missing componentSetId parameter");
4768
+ if (!propertyName) throw new Error("Missing propertyName parameter");
4769
+ if (defaultValue === undefined) defaultValue = true;
4770
+ if (!layerName) throw new Error("Missing layerName parameter");
4771
+
4772
+ var componentSet = await figma.getNodeByIdAsync(componentSetId);
4773
+ if (!componentSet) throw new Error("Node not found: " + componentSetId);
4774
+ if (componentSet.type !== "COMPONENT_SET" && componentSet.type !== "COMPONENT") {
4775
+ throw new Error("Node must be COMPONENT_SET or COMPONENT, got " + componentSet.type);
4776
+ }
4777
+
4778
+ // Check if property already exists
4779
+ var existingProps = componentSet.componentPropertyDefinitions;
4780
+ var propertyKey = null;
4781
+ for (var key in existingProps) {
4782
+ if (existingProps.hasOwnProperty(key)) {
4783
+ var def = existingProps[key];
4784
+ if (def.type === "BOOLEAN" && key.startsWith(propertyName + "#")) {
4785
+ propertyKey = key;
4786
+ break;
4787
+ }
4788
+ }
4789
+ }
4790
+
4791
+ if (!propertyKey) {
4792
+ propertyKey = componentSet.addComponentProperty(propertyName, "BOOLEAN", defaultValue);
4793
+ }
4794
+
4795
+ // Find layers matching layerName across all variants and link visibility
4796
+ var components = [];
4797
+ if (componentSet.type === "COMPONENT_SET") {
4798
+ components = componentSet.children.filter(function(c) { return c.type === "COMPONENT"; });
4799
+ } else {
4800
+ components = [componentSet];
4801
+ }
4802
+
4803
+ var linked = 0;
4804
+ var errors = [];
4805
+ for (var i = 0; i < components.length; i++) {
4806
+ var comp = components[i];
4807
+ try {
4808
+ var layer = comp.findOne(function(n) { return n.name === layerName; });
4809
+ if (layer) {
4810
+ var existingRefs = layer.componentPropertyReferences || {};
4811
+ var newRefs = Object.assign({}, existingRefs);
4812
+ newRefs.visible = propertyKey;
4813
+ layer.componentPropertyReferences = newRefs;
4814
+ linked++;
4815
+ }
4816
+ } catch (err) {
4817
+ errors.push(comp.id + ": " + (err.message || String(err)));
4818
+ }
4819
+ }
4820
+
4821
+ return {
4822
+ success: true,
4823
+ propertyKey: propertyKey,
4824
+ linked: linked,
4825
+ totalVariants: components.length,
4826
+ errors: errors,
4827
+ message: "Added BOOLEAN property '" + propertyName + "', linked " + linked + " layers",
4828
+ };
4829
+ }
4830
+
4831
+ async function addInstanceSwapProperty(params) {
4832
+ var componentSetId = (params || {}).componentSetId;
4833
+ var propertyName = (params || {}).propertyName;
4834
+ var defaultComponentId = (params || {}).defaultComponentId;
4835
+ var layerName = (params || {}).layerName;
4836
+
4837
+ if (!componentSetId) throw new Error("Missing componentSetId parameter");
4838
+ if (!propertyName) throw new Error("Missing propertyName parameter");
4839
+ if (!defaultComponentId) throw new Error("Missing defaultComponentId parameter");
4840
+ if (!layerName) throw new Error("Missing layerName parameter");
4841
+
4842
+ var componentSet = await figma.getNodeByIdAsync(componentSetId);
4843
+ if (!componentSet) throw new Error("Node not found: " + componentSetId);
4844
+ if (componentSet.type !== "COMPONENT_SET" && componentSet.type !== "COMPONENT") {
4845
+ throw new Error("Node must be COMPONENT_SET or COMPONENT, got " + componentSet.type);
4846
+ }
4847
+
4848
+ // Get the default component for the instance swap
4849
+ var defaultComponent = await figma.getNodeByIdAsync(defaultComponentId);
4850
+ if (!defaultComponent || defaultComponent.type !== "COMPONENT") {
4851
+ throw new Error("Default component not found or not a COMPONENT: " + defaultComponentId);
4852
+ }
4853
+
4854
+ // Check if property already exists
4855
+ var existingProps = componentSet.componentPropertyDefinitions;
4856
+ var propertyKey = null;
4857
+ for (var key in existingProps) {
4858
+ if (existingProps.hasOwnProperty(key)) {
4859
+ var def = existingProps[key];
4860
+ if (def.type === "INSTANCE_SWAP" && key.startsWith(propertyName + "#")) {
4861
+ propertyKey = key;
4862
+ break;
4863
+ }
4864
+ }
4865
+ }
4866
+
4867
+ if (!propertyKey) {
4868
+ propertyKey = componentSet.addComponentProperty(propertyName, "INSTANCE_SWAP", defaultComponent.id);
4869
+ }
4870
+
4871
+ // Find instance layers matching layerName across all variants and link
4872
+ var components = [];
4873
+ if (componentSet.type === "COMPONENT_SET") {
4874
+ components = componentSet.children.filter(function(c) { return c.type === "COMPONENT"; });
4875
+ } else {
4876
+ components = [componentSet];
4877
+ }
4878
+
4879
+ var linked = 0;
4880
+ var errors = [];
4881
+ for (var i = 0; i < components.length; i++) {
4882
+ var comp = components[i];
4883
+ try {
4884
+ var layer = comp.findOne(function(n) { return n.name === layerName; });
4885
+ if (layer && layer.type === "INSTANCE") {
4886
+ var existingRefs = layer.componentPropertyReferences || {};
4887
+ var newRefs = Object.assign({}, existingRefs);
4888
+ newRefs.mainComponent = propertyKey;
4889
+ layer.componentPropertyReferences = newRefs;
4890
+ linked++;
4891
+ }
4892
+ } catch (err) {
4893
+ errors.push(comp.id + ": " + (err.message || String(err)));
4894
+ }
4895
+ }
4896
+
4897
+ return {
4898
+ success: true,
4899
+ propertyKey: propertyKey,
4900
+ linked: linked,
4901
+ totalVariants: components.length,
4902
+ errors: errors,
4903
+ message: "Added INSTANCE_SWAP property '" + propertyName + "', linked " + linked + " instances",
4904
+ };
4905
+ }
4906
+
4907
+ async function syncLayerFills(params) {
4908
+ var componentSetId = (params || {}).componentSetId;
4909
+ var sourceLayerName = (params || {}).sourceLayerName;
4910
+ var targetLayerName = (params || {}).targetLayerName;
4911
+
4912
+ if (!componentSetId) throw new Error("Missing componentSetId parameter");
4913
+ if (!sourceLayerName) throw new Error("Missing sourceLayerName parameter");
4914
+ if (!targetLayerName) throw new Error("Missing targetLayerName parameter");
4915
+
4916
+ var componentSet = await figma.getNodeByIdAsync(componentSetId);
4917
+ if (!componentSet) throw new Error("Node not found: " + componentSetId);
4918
+ if (componentSet.type !== "COMPONENT_SET" && componentSet.type !== "COMPONENT") {
4919
+ throw new Error("Node must be COMPONENT_SET or COMPONENT, got " + componentSet.type);
4920
+ }
4921
+
4922
+ var components = [];
4923
+ if (componentSet.type === "COMPONENT_SET") {
4924
+ components = componentSet.children.filter(function(c) { return c.type === "COMPONENT"; });
4925
+ } else {
4926
+ components = [componentSet];
4927
+ }
4928
+
4929
+ function applyFillsRecursive(node, fills, boundVarFills) {
4930
+ // Apply fills to nodes that support fills (not groups, not pages)
4931
+ if ("fills" in node && node.type !== "GROUP") {
4932
+ try {
4933
+ node.fills = fills;
4934
+ // Copy variable bindings for fills if available
4935
+ if (boundVarFills && boundVarFills.length > 0) {
4936
+ for (var fi = 0; fi < boundVarFills.length; fi++) {
4937
+ var bv = boundVarFills[fi];
4938
+ if (bv && bv.id) {
4939
+ figma.variables.getVariableByIdAsync(bv.id).then(function(variable) {
4940
+ if (variable) {
4941
+ try { node.setBoundVariable("fills", fi, variable); } catch(e) {}
4942
+ }
4943
+ });
4944
+ }
4945
+ }
4946
+ }
4947
+ } catch (e) {
4948
+ // Some nodes may not support setting fills
4949
+ }
4950
+ }
4951
+ // Recurse into children
4952
+ if ("children" in node) {
4953
+ for (var ci = 0; ci < node.children.length; ci++) {
4954
+ applyFillsRecursive(node.children[ci], fills, boundVarFills);
4955
+ }
4956
+ }
4957
+ }
4958
+
4959
+ var synced = 0;
4960
+ var errors = [];
4961
+ for (var i = 0; i < components.length; i++) {
4962
+ var comp = components[i];
4963
+ try {
4964
+ var sourceLayer = comp.findOne(function(n) { return n.name === sourceLayerName; });
4965
+ var targetLayer = comp.findOne(function(n) { return n.name === targetLayerName; });
4966
+ if (!sourceLayer) {
4967
+ errors.push(comp.id + ": source layer '" + sourceLayerName + "' not found");
4968
+ continue;
4969
+ }
4970
+ if (!targetLayer) {
4971
+ errors.push(comp.id + ": target layer '" + targetLayerName + "' not found");
4972
+ continue;
4973
+ }
4974
+ if (!("fills" in sourceLayer)) {
4975
+ errors.push(comp.id + ": source layer has no fills");
4976
+ continue;
4977
+ }
4978
+
4979
+ var sourceFills = JSON.parse(JSON.stringify(sourceLayer.fills));
4980
+ // Get bound variable references for fills
4981
+ var boundVarFills = [];
4982
+ try {
4983
+ var bv = sourceLayer.boundVariables;
4984
+ if (bv && bv.fills) {
4985
+ boundVarFills = bv.fills;
4986
+ }
4987
+ } catch (e) {}
4988
+
4989
+ applyFillsRecursive(targetLayer, sourceFills, boundVarFills);
4990
+ synced++;
4991
+ } catch (err) {
4992
+ errors.push(comp.id + ": " + (err.message || String(err)));
4993
+ }
4994
+ }
4995
+
4996
+ return {
4997
+ success: true,
4998
+ synced: synced,
4999
+ totalVariants: components.length,
5000
+ errors: errors,
5001
+ message: "Synced fills from '" + sourceLayerName + "' to '" + targetLayerName + "' in " + synced + " variants",
5002
+ };
5003
+ }
5004
+
5005
+ async function moveChildToIndex(params) {
5006
+ var componentSetId = (params || {}).componentSetId;
5007
+ var layerName = (params || {}).layerName;
5008
+ var targetIndex = (params || {}).targetIndex;
5009
+
5010
+ if (!componentSetId) throw new Error("Missing componentSetId parameter");
5011
+ if (!layerName) throw new Error("Missing layerName parameter");
5012
+ if (targetIndex === undefined) throw new Error("Missing targetIndex parameter");
5013
+
5014
+ var componentSet = await figma.getNodeByIdAsync(componentSetId);
5015
+ if (!componentSet) throw new Error("Node not found: " + componentSetId);
5016
+ if (componentSet.type !== "COMPONENT_SET" && componentSet.type !== "COMPONENT") {
5017
+ throw new Error("Node must be COMPONENT_SET or COMPONENT, got " + componentSet.type);
5018
+ }
5019
+
5020
+ var components = [];
5021
+ if (componentSet.type === "COMPONENT_SET") {
5022
+ components = componentSet.children.filter(function(c) { return c.type === "COMPONENT"; });
5023
+ } else {
5024
+ components = [componentSet];
5025
+ }
5026
+
5027
+ var moved = 0;
5028
+ var errors = [];
5029
+ for (var i = 0; i < components.length; i++) {
5030
+ var comp = components[i];
5031
+ try {
5032
+ var layer = comp.findOne(function(n) { return n.name === layerName; });
5033
+ if (layer) {
5034
+ comp.insertChild(targetIndex, layer);
5035
+ moved++;
5036
+ }
5037
+ } catch (err) {
5038
+ errors.push(comp.id + ": " + (err.message || String(err)));
5039
+ }
5040
+ }
5041
+
5042
+ return {
5043
+ success: true,
5044
+ moved: moved,
5045
+ totalVariants: components.length,
5046
+ errors: errors,
5047
+ message: "Moved '" + layerName + "' to index " + targetIndex + " in " + moved + " variants",
5048
+ };
5049
+ }
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@biolab/talk-to-figma",
3
3
  "description": "Talk to Figma MCP Server",
4
- "version": "0.6.0",
4
+ "version": "0.8.0",
5
5
  "module": "dist/server.js",
6
6
  "main": "dist/server.js",
7
7
  "bin": {