@mp3wizard/figma-console-mcp 1.32.3 → 1.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +25 -17
  2. package/dist/cloudflare/core/cloud-websocket-connector.js +18 -0
  3. package/dist/cloudflare/core/design-system-manifest.js +19 -14
  4. package/dist/cloudflare/core/design-system-tools.js +43 -34
  5. package/dist/cloudflare/core/diagnose-tool.js +4 -0
  6. package/dist/cloudflare/core/enrichment/enrichment-service.js +11 -5
  7. package/dist/cloudflare/core/enrichment/style-resolver.js +38 -18
  8. package/dist/cloudflare/core/figma-api.js +118 -54
  9. package/dist/cloudflare/core/figma-tools.js +179 -63
  10. package/dist/cloudflare/core/port-discovery.js +404 -31
  11. package/dist/cloudflare/core/tokens/alias-resolver.js +75 -5
  12. package/dist/cloudflare/core/tokens/config.js +10 -0
  13. package/dist/cloudflare/core/tokens/dialect.js +232 -0
  14. package/dist/cloudflare/core/tokens/figma-converter.js +144 -16
  15. package/dist/cloudflare/core/tokens/formatters/css-vars.js +21 -12
  16. package/dist/cloudflare/core/tokens/formatters/dtcg.js +106 -30
  17. package/dist/cloudflare/core/tokens/formatters/json.js +28 -10
  18. package/dist/cloudflare/core/tokens/formatters/scss.js +19 -13
  19. package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +15 -9
  20. package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +14 -9
  21. package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +11 -5
  22. package/dist/cloudflare/core/tokens/index.js +2 -1
  23. package/dist/cloudflare/core/tokens/parsers/dtcg.js +32 -5
  24. package/dist/cloudflare/core/tokens/schemas.js +4 -0
  25. package/dist/cloudflare/core/tokens-tools.js +1017 -88
  26. package/dist/cloudflare/core/version-tools.js +44 -3
  27. package/dist/cloudflare/core/websocket-connector.js +42 -0
  28. package/dist/cloudflare/core/websocket-server.js +99 -8
  29. package/dist/cloudflare/core/write-tools.js +355 -86
  30. package/dist/cloudflare/index.js +7 -7
  31. package/dist/core/design-system-manifest.d.ts +1 -0
  32. package/dist/core/design-system-manifest.d.ts.map +1 -1
  33. package/dist/core/design-system-manifest.js +19 -14
  34. package/dist/core/design-system-manifest.js.map +1 -1
  35. package/dist/core/design-system-tools.d.ts.map +1 -1
  36. package/dist/core/design-system-tools.js +43 -34
  37. package/dist/core/design-system-tools.js.map +1 -1
  38. package/dist/core/diagnose-tool.d.ts +8 -0
  39. package/dist/core/diagnose-tool.d.ts.map +1 -1
  40. package/dist/core/diagnose-tool.js +4 -0
  41. package/dist/core/diagnose-tool.js.map +1 -1
  42. package/dist/core/enrichment/enrichment-service.d.ts.map +1 -1
  43. package/dist/core/enrichment/enrichment-service.js +11 -5
  44. package/dist/core/enrichment/enrichment-service.js.map +1 -1
  45. package/dist/core/enrichment/style-resolver.d.ts +7 -2
  46. package/dist/core/enrichment/style-resolver.d.ts.map +1 -1
  47. package/dist/core/enrichment/style-resolver.js +38 -18
  48. package/dist/core/enrichment/style-resolver.js.map +1 -1
  49. package/dist/core/figma-api.d.ts +18 -9
  50. package/dist/core/figma-api.d.ts.map +1 -1
  51. package/dist/core/figma-api.js +118 -54
  52. package/dist/core/figma-api.js.map +1 -1
  53. package/dist/core/figma-connector.d.ts +12 -0
  54. package/dist/core/figma-connector.d.ts.map +1 -1
  55. package/dist/core/figma-tools.d.ts.map +1 -1
  56. package/dist/core/figma-tools.js +179 -63
  57. package/dist/core/figma-tools.js.map +1 -1
  58. package/dist/core/port-discovery.d.ts +40 -0
  59. package/dist/core/port-discovery.d.ts.map +1 -1
  60. package/dist/core/port-discovery.js +404 -31
  61. package/dist/core/port-discovery.js.map +1 -1
  62. package/dist/core/tokens/alias-resolver.d.ts +45 -3
  63. package/dist/core/tokens/alias-resolver.d.ts.map +1 -1
  64. package/dist/core/tokens/alias-resolver.js +75 -5
  65. package/dist/core/tokens/alias-resolver.js.map +1 -1
  66. package/dist/core/tokens/config.d.ts +28 -0
  67. package/dist/core/tokens/config.d.ts.map +1 -1
  68. package/dist/core/tokens/config.js +10 -0
  69. package/dist/core/tokens/config.js.map +1 -1
  70. package/dist/core/tokens/dialect.d.ts +107 -0
  71. package/dist/core/tokens/dialect.d.ts.map +1 -0
  72. package/dist/core/tokens/dialect.js +233 -0
  73. package/dist/core/tokens/dialect.js.map +1 -0
  74. package/dist/core/tokens/figma-converter.d.ts +23 -2
  75. package/dist/core/tokens/figma-converter.d.ts.map +1 -1
  76. package/dist/core/tokens/figma-converter.js +144 -16
  77. package/dist/core/tokens/figma-converter.js.map +1 -1
  78. package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -1
  79. package/dist/core/tokens/formatters/css-vars.js +21 -12
  80. package/dist/core/tokens/formatters/css-vars.js.map +1 -1
  81. package/dist/core/tokens/formatters/dtcg.d.ts +2 -2
  82. package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -1
  83. package/dist/core/tokens/formatters/dtcg.js +106 -30
  84. package/dist/core/tokens/formatters/dtcg.js.map +1 -1
  85. package/dist/core/tokens/formatters/json.d.ts.map +1 -1
  86. package/dist/core/tokens/formatters/json.js +28 -10
  87. package/dist/core/tokens/formatters/json.js.map +1 -1
  88. package/dist/core/tokens/formatters/scss.d.ts.map +1 -1
  89. package/dist/core/tokens/formatters/scss.js +19 -13
  90. package/dist/core/tokens/formatters/scss.js.map +1 -1
  91. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -1
  92. package/dist/core/tokens/formatters/style-dictionary-v3.js +15 -9
  93. package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -1
  94. package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -1
  95. package/dist/core/tokens/formatters/tailwind-v4.js +14 -9
  96. package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -1
  97. package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -1
  98. package/dist/core/tokens/formatters/tokens-studio.js +11 -5
  99. package/dist/core/tokens/formatters/tokens-studio.js.map +1 -1
  100. package/dist/core/tokens/index.d.ts +2 -1
  101. package/dist/core/tokens/index.d.ts.map +1 -1
  102. package/dist/core/tokens/index.js +2 -1
  103. package/dist/core/tokens/index.js.map +1 -1
  104. package/dist/core/tokens/parsers/dtcg.js +32 -5
  105. package/dist/core/tokens/parsers/dtcg.js.map +1 -1
  106. package/dist/core/tokens/schemas.d.ts +3 -0
  107. package/dist/core/tokens/schemas.d.ts.map +1 -1
  108. package/dist/core/tokens/schemas.js +4 -0
  109. package/dist/core/tokens/schemas.js.map +1 -1
  110. package/dist/core/tokens/types.d.ts +57 -1
  111. package/dist/core/tokens/types.d.ts.map +1 -1
  112. package/dist/core/tokens/types.js.map +1 -1
  113. package/dist/core/tokens-tools.d.ts +250 -7
  114. package/dist/core/tokens-tools.d.ts.map +1 -1
  115. package/dist/core/tokens-tools.js +1017 -88
  116. package/dist/core/tokens-tools.js.map +1 -1
  117. package/dist/core/version-tools.d.ts.map +1 -1
  118. package/dist/core/version-tools.js +44 -3
  119. package/dist/core/version-tools.js.map +1 -1
  120. package/dist/core/websocket-connector.d.ts +38 -0
  121. package/dist/core/websocket-connector.d.ts.map +1 -1
  122. package/dist/core/websocket-connector.js +42 -0
  123. package/dist/core/websocket-connector.js.map +1 -1
  124. package/dist/core/websocket-server.d.ts +23 -0
  125. package/dist/core/websocket-server.d.ts.map +1 -1
  126. package/dist/core/websocket-server.js +99 -8
  127. package/dist/core/websocket-server.js.map +1 -1
  128. package/dist/core/write-tools.d.ts.map +1 -1
  129. package/dist/core/write-tools.js +355 -86
  130. package/dist/core/write-tools.js.map +1 -1
  131. package/dist/local.d.ts +0 -1
  132. package/dist/local.d.ts.map +1 -1
  133. package/dist/local.js +253 -63
  134. package/dist/local.js.map +1 -1
  135. package/figma-desktop-bridge/code.js +382 -28
  136. package/figma-desktop-bridge/ui.html +578 -292
  137. package/package.json +2 -2
@@ -697,7 +697,7 @@ return {
697
697
  }
698
698
  });
699
699
  // Tool: Setup design tokens (collection + modes + variables atomically)
700
- server.tool("figma_setup_design_tokens", "Create a complete design token structure in one operation: collection, modes, and all variables. Ideal for importing CSS custom properties or design tokens into Figma. Requires Desktop Bridge plugin.", {
700
+ server.tool("figma_setup_design_tokens", "Create a complete design token structure in one operation: collection, modes, and all variables. Ideal for importing CSS custom properties or design tokens into Figma. Values may be literals OR DTCG-style brace references ('{color.blue.600}', or set-qualified '{primitives.color.blue.600}') that resolve to variable ALIASES — first against variables created in this same call, then against existing local variables. Unresolvable references skip that value with a per-item warning (the rest of the batch still applies), so semantic collections referencing primitives no longer need raw figma_execute. Requires Desktop Bridge plugin.", {
701
701
  collectionName: z
702
702
  .string()
703
703
  .describe("Name for the token collection (e.g., 'Brand Tokens')"),
@@ -720,7 +720,7 @@ return {
720
720
  .describe("Optional description"),
721
721
  values: z
722
722
  .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
723
- .describe("Values keyed by mode NAME (not ID). Example: { 'Light': '#FFFFFF', 'Dark': '#000000' }"),
723
+ .describe("Values keyed by mode NAME (not ID). Example: { 'Light': '#FFFFFF', 'Dark': '#000000' }. A string value wrapped in braces is an ALIAS reference resolved to another variable: '{color.blue.600}' matches variable name 'color/blue/600' — first among variables created in THIS call, then existing local variables (exact match, then case-insensitive). Set-qualified references ('{primitives.color.blue.600}') strip the leading collection name. Unresolvable references skip that value and surface in the response's warnings[]."),
724
724
  }))
725
725
  .min(1)
726
726
  .max(100)
@@ -758,31 +758,130 @@ for (let i = 1; i < modeNames.length; i++) {
758
758
  modeMap[modeNames[i]] = newModeId;
759
759
  }
760
760
 
761
- // Step 3: Create all variables with values
761
+ // Step 3: Create all variables FIRST (values apply in a second pass so
762
+ // brace-reference values can alias variables created later in this call).
762
763
  const results = [];
764
+ const warnings = [];
765
+ const resultByName = {}; // ONE results entry per token name — value-phase
766
+ // problems attach to it as valueErrors instead of
767
+ // pushing extra entries (which inflated created+failed
768
+ // past the token count).
769
+ const createdByName = {}; // exact name -> variable (this call)
770
+ const createdByLower = {}; // lowercased name -> variable (this call)
771
+ const createdDefs = []; // { def, variable } for the value pass
763
772
  for (const t of tokenDefs) {
764
773
  try {
765
774
  const variable = figma.variables.createVariable(t.name, collection, t.resolvedType);
766
775
  if (t.description) variable.description = t.description;
767
- for (const [modeName, value] of Object.entries(t.values)) {
768
- const modeId = modeMap[modeName];
769
- if (!modeId) { results.push({ success: false, name: t.name, error: 'Unknown mode: ' + modeName }); continue; }
770
- const processed = t.resolvedType === 'COLOR' && typeof value === 'string' ? hexToRgba(value) : value;
771
- variable.setValueForMode(modeId, processed);
772
- }
773
- results.push({ success: true, name: t.name, id: variable.id });
776
+ createdByName[t.name] = variable;
777
+ createdByLower[t.name.toLowerCase()] = variable;
778
+ createdDefs.push({ def: t, variable: variable });
779
+ const entry = { success: true, name: t.name, id: variable.id };
780
+ resultByName[t.name] = entry;
781
+ results.push(entry);
774
782
  } catch (err) {
775
783
  results.push({ success: false, name: t.name, error: String(err) });
776
784
  }
777
785
  }
778
786
 
787
+ // Reference resolution: '{color.blue.600}' -> variable named 'color/blue/600'.
788
+ // Priority: created-in-this-call (exact, then case-insensitive; optionally
789
+ // set-qualified by THIS collection's name), then existing local variables
790
+ // (exact, then case-insensitive; optionally set-qualified by an existing
791
+ // collection's name).
792
+ function isReference(value) {
793
+ return typeof value === 'string' && /^\\{[^{}]+\\}$/.test(value.trim());
794
+ }
795
+ let existingVars = null;
796
+ let existingCollections = null;
797
+ async function ensureExistingLoaded() {
798
+ if (existingVars === null) {
799
+ existingVars = await figma.variables.getLocalVariablesAsync();
800
+ existingCollections = await figma.variables.getLocalVariableCollectionsAsync();
801
+ }
802
+ }
803
+ function findCreated(name) {
804
+ return createdByName[name] || createdByLower[name.toLowerCase()] || null;
805
+ }
806
+ async function resolveReference(raw) {
807
+ const inner = raw.trim().slice(1, -1);
808
+ const segments = inner.split('.').filter(function (s) { return s.length > 0; });
809
+ if (segments.length === 0) return null;
810
+ const fullName = segments.join('/');
811
+
812
+ // 1. Variables created in THIS call.
813
+ let match = findCreated(fullName);
814
+ if (match) return match;
815
+ if (segments.length > 1 && segments[0].toLowerCase() === collectionName.toLowerCase()) {
816
+ match = findCreated(segments.slice(1).join('/'));
817
+ if (match) return match;
818
+ }
819
+
820
+ // 2. Existing local variables.
821
+ await ensureExistingLoaded();
822
+ match = existingVars.find(function (v) { return v.name === fullName; });
823
+ if (match) return match;
824
+ const lowerName = fullName.toLowerCase();
825
+ match = existingVars.find(function (v) { return v.name.toLowerCase() === lowerName; });
826
+ if (match) return match;
827
+ // 2b. Set-qualified: first segment names an existing collection.
828
+ if (segments.length > 1) {
829
+ const restName = segments.slice(1).join('/');
830
+ const restLower = restName.toLowerCase();
831
+ const setLower = segments[0].toLowerCase();
832
+ for (const c of existingCollections) {
833
+ if (c.name.toLowerCase() !== setLower) continue;
834
+ let m = existingVars.find(function (v) { return v.variableCollectionId === c.id && v.name === restName; });
835
+ if (!m) m = existingVars.find(function (v) { return v.variableCollectionId === c.id && v.name.toLowerCase() === restLower; });
836
+ if (m) return m;
837
+ }
838
+ }
839
+ return null;
840
+ }
841
+
842
+ // Step 4: Apply values — literals directly, brace references as aliases.
843
+ // Value-phase problems attach to the token's EXISTING results entry (as
844
+ // valueErrors) + warnings; they never add a second entry for the same name.
845
+ function noteValueProblem(name, message) {
846
+ const entry = resultByName[name];
847
+ if (entry) {
848
+ entry.valueErrors = entry.valueErrors || [];
849
+ entry.valueErrors.push(message);
850
+ }
851
+ warnings.push('Token "' + name + '": ' + message);
852
+ }
853
+ for (const entry of createdDefs) {
854
+ const t = entry.def;
855
+ const variable = entry.variable;
856
+ for (const [modeName, value] of Object.entries(t.values)) {
857
+ const modeId = modeMap[modeName];
858
+ if (!modeId) { noteValueProblem(t.name, 'Unknown mode: ' + modeName + ' — value skipped.'); continue; }
859
+ try {
860
+ if (isReference(value)) {
861
+ const target = await resolveReference(value);
862
+ if (!target) {
863
+ warnings.push('Unresolvable reference ' + value + ' for token "' + t.name + '" (mode "' + modeName + '") — no matching variable in this call or the local file. Value skipped.');
864
+ continue;
865
+ }
866
+ variable.setValueForMode(modeId, figma.variables.createVariableAlias(target));
867
+ } else {
868
+ const processed = t.resolvedType === 'COLOR' && typeof value === 'string' ? hexToRgba(value) : value;
869
+ variable.setValueForMode(modeId, processed);
870
+ }
871
+ } catch (err) {
872
+ noteValueProblem(t.name, 'mode "' + modeName + '": ' + String(err));
873
+ }
874
+ }
875
+ }
876
+
779
877
  return {
780
878
  collectionId: collection.id,
781
879
  collectionName: collectionName,
782
880
  modes: modeMap,
783
881
  created: results.filter(r => r.success).length,
784
882
  failed: results.filter(r => !r.success).length,
785
- results
883
+ results,
884
+ warnings
786
885
  };`;
787
886
  const timeout = Math.max(10000, tokens.length * 200 + modes.length * 500);
788
887
  const result = await connector.executeCodeViaUI(script, Math.min(timeout, 30000));
@@ -1601,47 +1700,15 @@ After instantiating components, use figma_take_screenshot to verify the result l
1601
1700
  // ============================================================================
1602
1701
  // Component Set Arrangement Tool
1603
1702
  // ============================================================================
1604
- // Tool: Arrange Component Set (Professional Layout with Native Visualization)
1605
- // Recreates component set using figma.combineAsVariants() for proper purple dashed frame
1606
- server.tool("figma_arrange_component_set", `Organize a component set with Figma's native purple dashed visualization. Use after creating variants, adding states (hover/disabled/pressed), or when component sets need cleanup.
1607
-
1608
- Recreates the set using figma.combineAsVariants() for proper Figma integration, applies purple dashed border styling, and arranges variants in a labeled grid (columns = last property like State, rows = other properties like Type+Size). Creates a white container with title, row/column labels, and the component set.`, {
1609
- componentSetId: z
1610
- .string()
1611
- .optional()
1612
- .describe("Node ID of the component set to arrange. If not provided, will look for a selected component set."),
1613
- componentSetName: z
1614
- .string()
1615
- .optional()
1616
- .describe("Name of the component set to find. Used if componentSetId not provided."),
1617
- options: z
1618
- .object({
1619
- gap: z
1620
- .number()
1621
- .optional()
1622
- .default(24)
1623
- .describe("Gap between grid cells in pixels (default: 24)"),
1624
- cellPadding: z
1625
- .number()
1626
- .optional()
1627
- .default(20)
1628
- .describe("Padding inside each cell around the variant (default: 20)"),
1629
- columnProperty: z
1630
- .string()
1631
- .optional()
1632
- .describe("Property to use for columns (default: auto-detect last property, usually 'State')"),
1633
- })
1634
- .optional()
1635
- .describe("Layout options"),
1636
- }, async ({ componentSetId, componentSetName, options }) => {
1637
- try {
1638
- const connector = await getDesktopConnector();
1639
- // Build the code to execute in Figma
1640
- const code = `
1703
+ // Builds the in-place grid arrangement script. Shared by
1704
+ // figma_arrange_component_set and figma_create_component_set (autoArrange).
1705
+ const buildArrangeComponentSetCode = (componentSetId, componentSetName, options) => `
1641
1706
  // ============================================================================
1642
1707
  // COMPONENT SET ARRANGEMENT WITH PROPER LABELS AND CONTAINER
1643
1708
  // Creates: White container frame -> Row labels (left) -> Column headers (top) -> Component set (center)
1644
1709
  // Uses auto-layout for proper alignment of labels with grid cells
1710
+ // NON-DESTRUCTIVE: variants are repositioned in place on the existing set, so
1711
+ // the set's node ID is preserved and placed instances are unaffected.
1645
1712
  // ============================================================================
1646
1713
 
1647
1714
  // Configuration
@@ -1679,8 +1746,10 @@ if (!componentSet || componentSet.type !== "COMPONENT_SET") {
1679
1746
  }
1680
1747
 
1681
1748
  const page = figma.currentPage;
1682
- const csOriginalX = componentSet.x;
1683
- const csOriginalY = componentSet.y;
1749
+ // Absolute page coordinates — the set may be nested inside a previous
1750
+ // arrangement container, so relative x/y would be wrong for placement
1751
+ const csAbsX = componentSet.absoluteTransform[0][2];
1752
+ const csAbsY = componentSet.absoluteTransform[1][2];
1684
1753
  const csOriginalName = componentSet.name;
1685
1754
 
1686
1755
  // Get all variant components
@@ -1766,46 +1835,50 @@ const csHeight = (totalRows * cellHeight) + ((totalRows - 1) * gap) + (edgePaddi
1766
1835
  // ============================================================================
1767
1836
  // STEP 1: Remove old labels and container frames from previous arrangements
1768
1837
  // ============================================================================
1769
- const oldElements = page.children.filter(n =>
1838
+ const isOldArrangementElement = (n) =>
1770
1839
  (n.type === "TEXT" && (n.name.startsWith("Row: ") || n.name.startsWith("Col: "))) ||
1771
- (n.type === "FRAME" && (n.name === "Component Container" || n.name === "Row Labels" || n.name === "Column Headers"))
1772
- );
1840
+ (n.type === "FRAME" && (n.name === "Component Container" || n.name === "Row Labels" || n.name === "Column Headers"));
1841
+
1842
+ // If the set lives inside a previous arrangement container (re-run case),
1843
+ // pull it out to the page first so cleanup can't delete it
1844
+ let ancestor = componentSet.parent;
1845
+ while (ancestor && ancestor.type !== "PAGE") {
1846
+ if (isOldArrangementElement(ancestor)) {
1847
+ page.appendChild(componentSet);
1848
+ break;
1849
+ }
1850
+ ancestor = ancestor.parent;
1851
+ }
1852
+
1853
+ const oldElements = page.children.filter(isOldArrangementElement);
1773
1854
  for (const el of oldElements) {
1774
1855
  el.remove();
1775
1856
  }
1776
1857
 
1777
1858
  // ============================================================================
1778
- // STEP 2: Clone variants and recreate component set with native visualization
1859
+ // STEP 2: Prepare the EXISTING component set for manual grid positioning.
1860
+ // No clone/remove/combineAsVariants — the set keeps its node ID, so all
1861
+ // placed instances survive.
1779
1862
  // ============================================================================
1780
- const clonedVariants = [];
1781
- for (const variant of variants) {
1782
- const clone = variant.clone();
1783
- page.appendChild(clone);
1784
- clonedVariants.push(clone);
1785
- }
1786
-
1787
- // Delete the old component set
1788
- componentSet.remove();
1789
1863
 
1790
- // Recreate using figma.combineAsVariants() for native purple dashed frame
1791
- const newComponentSet = figma.combineAsVariants(clonedVariants, page);
1792
- newComponentSet.name = csOriginalName;
1864
+ // Auto-layout would ignore manual x/y on children — disable it if present
1865
+ if ("layoutMode" in componentSet && componentSet.layoutMode !== "NONE") {
1866
+ componentSet.layoutMode = "NONE";
1867
+ }
1793
1868
 
1794
1869
  // Apply purple dashed border (Figma's native component set styling)
1795
- newComponentSet.strokes = [{
1870
+ componentSet.strokes = [{
1796
1871
  type: 'SOLID',
1797
1872
  color: { r: 151/255, g: 71/255, b: 255/255 } // Figma's purple: #9747FF
1798
1873
  }];
1799
- newComponentSet.dashPattern = [10, 5];
1800
- newComponentSet.strokeWeight = 1;
1801
- newComponentSet.strokeAlign = "INSIDE";
1874
+ componentSet.dashPattern = [10, 5];
1875
+ componentSet.strokeWeight = 1;
1876
+ componentSet.strokeAlign = "INSIDE";
1802
1877
 
1803
1878
  // ============================================================================
1804
- // STEP 3: Arrange variants in grid pattern inside component set
1879
+ // STEP 3: Arrange variants in grid pattern inside component set (in place)
1805
1880
  // ============================================================================
1806
- const newVariants = newComponentSet.children.filter(n => n.type === "COMPONENT");
1807
-
1808
- for (const variant of newVariants) {
1881
+ for (const variant of variants) {
1809
1882
  const props = parseVariantName(variant.name);
1810
1883
  const colValue = props[columnProp];
1811
1884
  const colIdx = columnValues.indexOf(colValue);
@@ -1842,7 +1915,7 @@ for (const variant of newVariants) {
1842
1915
  }
1843
1916
 
1844
1917
  // Resize component set to fit grid
1845
- newComponentSet.resize(csWidth, csHeight);
1918
+ componentSet.resize(csWidth, csHeight);
1846
1919
 
1847
1920
  // ============================================================================
1848
1921
  // STEP 4: Create white container frame with proper structure
@@ -2026,10 +2099,11 @@ componentSetWrapper.name = "Component Set Wrapper";
2026
2099
  componentSetWrapper.fills = [];
2027
2100
  componentSetWrapper.resize(csWidth, csHeight);
2028
2101
 
2029
- // Move component set inside wrapper (positioned at 0,0)
2030
- componentSetWrapper.appendChild(newComponentSet);
2031
- newComponentSet.x = 0;
2032
- newComponentSet.y = 0;
2102
+ // Move the existing component set inside wrapper (reparenting preserves the
2103
+ // node ID — placed instances are unaffected)
2104
+ componentSetWrapper.appendChild(componentSet);
2105
+ componentSet.x = 0;
2106
+ componentSet.y = 0;
2033
2107
 
2034
2108
  // Append to parent FIRST, then set layoutSizing
2035
2109
  gridColumn.appendChild(componentSetWrapper);
@@ -2039,8 +2113,8 @@ componentSetWrapper.layoutSizingVertical = 'FIXED';
2039
2113
  contentRow.appendChild(gridColumn);
2040
2114
 
2041
2115
  // Position container at original location
2042
- containerFrame.x = csOriginalX - CONTAINER_PADDING - 120; // Account for row labels width
2043
- containerFrame.y = csOriginalY - CONTAINER_PADDING - TITLE_FONT_SIZE - 24 - COLUMN_HEADER_HEIGHT - gap;
2116
+ containerFrame.x = csAbsX - CONTAINER_PADDING - 120; // Account for row labels width
2117
+ containerFrame.y = csAbsY - CONTAINER_PADDING - TITLE_FONT_SIZE - 24 - COLUMN_HEADER_HEIGHT - gap;
2044
2118
 
2045
2119
  // Select and zoom to show result
2046
2120
  figma.currentPage.selection = [containerFrame];
@@ -2048,10 +2122,10 @@ figma.viewport.scrollAndZoomIntoView([containerFrame]);
2048
2122
 
2049
2123
  return {
2050
2124
  success: true,
2051
- message: "Component set arranged with proper container, labels, and alignment",
2125
+ message: "Component set arranged in place with proper container, labels, and alignment. Set identity preserved — placed instances unaffected.",
2052
2126
  containerId: containerFrame.id,
2053
- componentSetId: newComponentSet.id,
2054
- componentSetName: newComponentSet.name,
2127
+ componentSetId: componentSet.id,
2128
+ componentSetName: componentSet.name,
2055
2129
  grid: {
2056
2130
  rows: totalRows,
2057
2131
  columns: totalCols,
@@ -2064,7 +2138,7 @@ return {
2064
2138
  rowLabels: rowCombinations.map(combo => rowProps.map(p => combo[p]).join(" / "))
2065
2139
  },
2066
2140
  componentSetSize: { width: csWidth, height: csHeight },
2067
- variantCount: newVariants.length,
2141
+ variantCount: variants.length,
2068
2142
  structure: {
2069
2143
  container: "White frame with title, row labels, column headers, and component set",
2070
2144
  rowLabels: "Vertically aligned with each row's center",
@@ -2072,6 +2146,43 @@ return {
2072
2146
  }
2073
2147
  };
2074
2148
  `;
2149
+ // Tool: Arrange Component Set (Professional Layout with Native Visualization)
2150
+ // Rearranges variants IN PLACE (sets x/y on existing children) so the component
2151
+ // set's identity is preserved and placed instances are unaffected
2152
+ server.tool("figma_arrange_component_set", `Organize a component set with Figma's native purple dashed visualization. Use after creating variants, adding states (hover/disabled/pressed), or when component sets need cleanup.
2153
+
2154
+ Non-destructive: rearranges the existing variants in place (grid positions on the existing set's children), so the component set keeps its node ID and all placed instances remain intact. Arranges variants in a labeled grid (columns = last property like State, rows = other properties like Type+Size) and wraps the set in a white container with title, row/column labels. Safe to run on component sets with placed instances, and safe to re-run.`, {
2155
+ componentSetId: z
2156
+ .string()
2157
+ .optional()
2158
+ .describe("Node ID of the component set to arrange. If not provided, will look for a selected component set."),
2159
+ componentSetName: z
2160
+ .string()
2161
+ .optional()
2162
+ .describe("Name of the component set to find. Used if componentSetId not provided."),
2163
+ options: z
2164
+ .object({
2165
+ gap: z
2166
+ .number()
2167
+ .optional()
2168
+ .default(24)
2169
+ .describe("Gap between grid cells in pixels (default: 24)"),
2170
+ cellPadding: z
2171
+ .number()
2172
+ .optional()
2173
+ .default(20)
2174
+ .describe("Padding inside each cell around the variant (default: 20)"),
2175
+ columnProperty: z
2176
+ .string()
2177
+ .optional()
2178
+ .describe("Property to use for columns (default: auto-detect last property, usually 'State')"),
2179
+ })
2180
+ .optional()
2181
+ .describe("Layout options"),
2182
+ }, async ({ componentSetId, componentSetName, options }) => {
2183
+ try {
2184
+ const connector = await getDesktopConnector();
2185
+ const code = buildArrangeComponentSetCode(componentSetId ?? null, componentSetName ?? null, options);
2075
2186
  const result = await connector.executeCodeViaUI(code, 25000);
2076
2187
  if (!result.success) {
2077
2188
  throw new Error(result.error || "Failed to arrange component set");
@@ -2083,7 +2194,7 @@ return {
2083
2194
  text: JSON.stringify({
2084
2195
  ...result.result,
2085
2196
  hint: result.result?.success
2086
- ? "Component set arranged in a white container frame with properly aligned row and column labels. The purple dashed border is visible. Use figma_capture_screenshot to validate the layout."
2197
+ ? "Component set arranged in place (same node ID, placed instances unaffected) inside a white container frame with properly aligned row and column labels. The purple dashed border is visible. Use figma_capture_screenshot to validate the layout."
2087
2198
  : undefined,
2088
2199
  }),
2089
2200
  },
@@ -2106,6 +2217,164 @@ return {
2106
2217
  };
2107
2218
  }
2108
2219
  });
2220
+ // Tool: Create Component Set with variants (structured command — no hand-written
2221
+ // figma_execute scripts, no 30s execution-cap juggling for callers)
2222
+ server.tool("figma_create_component_set", `Create a component set with variants in one call — replaces hand-written figma.combineAsVariants scripts.
2223
+
2224
+ Two modes:
2225
+ 1. **Generate from a base component**: pass baseComponentId + properties (variant axes). The base is cloned for every combination of the axes ({ State: ['default','hover','disabled'], Size: ['sm','lg'] } → 6 variants), each named 'Prop=Value' comma-joined (e.g. 'State=hover, Size=sm'), then combined into a set. The base component itself becomes the FIRST variant (same node ID), so existing instances of the base survive as instances of that variant.
2226
+ 2. **Combine existing components**: pass componentIds, optionally with variantProperties (aligned 1:1) to rename each component to Prop=Value form before combining.
2227
+
2228
+ Figma derives the variant property definitions from the names; they live on the SET (componentPropertyDefinitions), not on individual variants. The result includes each variant's key — instantiate with a VARIANT's key/nodeId via figma_instantiate_component, not the set's key.
2229
+
2230
+ Set autoArrange:true to lay the new set out as a labeled grid inside a white container (same layout as figma_arrange_component_set). Requires Desktop Bridge plugin.
2231
+
2232
+ SIZE GUIDANCE: hard cap 100 variants. The timeout auto-scales with variant count (~1.2s/variant, 30s floor / 2min cap), but above ~40 variants the single-pass clone+combine gets slow and heavy base components may still push the limit — prefer splitting large matrices into multiple sets (e.g. one set per Size).`, {
2233
+ baseComponentId: z
2234
+ .string()
2235
+ .optional()
2236
+ .describe("Node ID of an existing COMPONENT to use as the base. Cloned per property combination; becomes the set's first variant (keeps its node ID, so placed instances survive). Mutually exclusive with componentIds."),
2237
+ properties: z
2238
+ .record(z.string(), z.array(z.string()))
2239
+ .optional()
2240
+ .describe("Variant property axes — required with baseComponentId. Example: { State: ['default','hover','disabled'], Size: ['sm','lg'] } creates 6 variants. Max 100 combinations. Names and values must not contain '=' or ','."),
2241
+ componentIds: z
2242
+ .array(z.string())
2243
+ .optional()
2244
+ .describe("Node IDs of existing COMPONENT nodes to combine as variants. Mutually exclusive with baseComponentId. Components already inside a component set are rejected."),
2245
+ variantProperties: z
2246
+ .array(z.record(z.string()))
2247
+ .optional()
2248
+ .describe("Only with componentIds: one property map per component, aligned by index — e.g. [{ State: 'default' }, { State: 'hover' }]. Each component is renamed to 'Prop=Value, ...' before combining. Without this, existing names are kept (names lacking '=' become 'Property 1=<name>')."),
2249
+ name: z
2250
+ .string()
2251
+ .optional()
2252
+ .describe("Name for the component set (e.g., 'Button'). Defaults to Figma's derived name."),
2253
+ parentId: z
2254
+ .string()
2255
+ .optional()
2256
+ .describe("Node ID of the container (frame/section) to create the set in. Defaults to the current page."),
2257
+ position: z
2258
+ .object({ x: z.number(), y: z.number() })
2259
+ .optional()
2260
+ .describe("Position of the set within its parent."),
2261
+ autoArrange: z
2262
+ .boolean()
2263
+ .optional()
2264
+ .default(false)
2265
+ .describe("If true, arrange the new set in a labeled grid (columns = last property, rows = other properties) inside a white container — same in-place layout as figma_arrange_component_set."),
2266
+ arrangeOptions: z
2267
+ .object({
2268
+ gap: z.number().optional().describe("Gap between grid cells in pixels (default: 24)"),
2269
+ cellPadding: z.number().optional().describe("Padding inside each cell around the variant (default: 20)"),
2270
+ columnProperty: z.string().optional().describe("Property to use for columns (default: last property)"),
2271
+ })
2272
+ .optional()
2273
+ .describe("Grid layout options, used when autoArrange is true."),
2274
+ }, async ({ baseComponentId, properties, componentIds, variantProperties, name, parentId, position, autoArrange, arrangeOptions, }) => {
2275
+ try {
2276
+ if (!baseComponentId && (!componentIds || componentIds.length === 0)) {
2277
+ throw new Error("Provide either baseComponentId + properties (generate variants from a base) or componentIds (combine existing components)");
2278
+ }
2279
+ if (baseComponentId && componentIds && componentIds.length > 0) {
2280
+ throw new Error("baseComponentId and componentIds are mutually exclusive — pick one mode");
2281
+ }
2282
+ if (baseComponentId &&
2283
+ (!properties || Object.keys(properties).length === 0)) {
2284
+ throw new Error("properties is required with baseComponentId. Example: { State: ['default','hover'], Size: ['sm','lg'] }");
2285
+ }
2286
+ // Variant count drives the connector's scaled timeout; above ~40 the
2287
+ // single-pass clone+combine gets slow, so surface a heads-up.
2288
+ let requestedVariantCount = componentIds?.length ?? 1;
2289
+ if (properties && !componentIds?.length) {
2290
+ requestedVariantCount = 1;
2291
+ for (const values of Object.values(properties)) {
2292
+ requestedVariantCount *= Math.max(1, values?.length ?? 1);
2293
+ }
2294
+ }
2295
+ const sizeWarning = requestedVariantCount > 40
2296
+ ? `${requestedVariantCount} variants requested — large sets are slow to build (timeout scales automatically, ~1.2s/variant, 2min cap) and heavy base components may still time out. Consider splitting into multiple sets (e.g. one per Size).`
2297
+ : undefined;
2298
+ const connector = await getDesktopConnector();
2299
+ const result = await connector.createComponentSet({
2300
+ baseComponentId,
2301
+ properties,
2302
+ componentIds,
2303
+ variantProperties,
2304
+ name,
2305
+ parentId,
2306
+ position,
2307
+ });
2308
+ if (!result.success) {
2309
+ throw new Error(result.error || "Failed to create component set");
2310
+ }
2311
+ const data = result.data || {};
2312
+ // Optional in-place grid arrangement — reuses the same script as
2313
+ // figma_arrange_component_set. Creation already succeeded, so an
2314
+ // arrange failure is reported as a warning, not an error.
2315
+ let arrange;
2316
+ if (autoArrange && data.componentSet?.id) {
2317
+ try {
2318
+ const arrangeResult = await connector.executeCodeViaUI(buildArrangeComponentSetCode(data.componentSet.id, null, arrangeOptions), 25000);
2319
+ if (arrangeResult.success &&
2320
+ arrangeResult.result &&
2321
+ !arrangeResult.result.error) {
2322
+ arrange = {
2323
+ arranged: true,
2324
+ containerId: arrangeResult.result.containerId,
2325
+ grid: arrangeResult.result.grid,
2326
+ };
2327
+ }
2328
+ else {
2329
+ arrange = {
2330
+ arranged: false,
2331
+ error: arrangeResult.error ||
2332
+ arrangeResult.result?.error ||
2333
+ "Arrange failed",
2334
+ };
2335
+ }
2336
+ }
2337
+ catch (arrangeError) {
2338
+ arrange = {
2339
+ arranged: false,
2340
+ error: arrangeError instanceof Error
2341
+ ? arrangeError.message
2342
+ : String(arrangeError),
2343
+ };
2344
+ }
2345
+ }
2346
+ return {
2347
+ content: [
2348
+ {
2349
+ type: "text",
2350
+ text: JSON.stringify({
2351
+ success: true,
2352
+ message: `Created component set "${data.componentSet?.name ?? name ?? ""}" with ${data.variantCount ?? "?"} variants`,
2353
+ ...data,
2354
+ ...(sizeWarning ? { sizeWarning } : {}),
2355
+ ...(arrange ? { arrange } : {}),
2356
+ hint: "To place instances, pass a VARIANT's key/nodeId from variants[] to figma_instantiate_component — not the set's key. Property definitions live on the set (componentPropertyDefinitions). Use figma_capture_screenshot to verify the result.",
2357
+ }),
2358
+ },
2359
+ ],
2360
+ };
2361
+ }
2362
+ catch (error) {
2363
+ logger.error({ error }, "Failed to create component set");
2364
+ return {
2365
+ content: [
2366
+ {
2367
+ type: "text",
2368
+ text: JSON.stringify({
2369
+ error: error instanceof Error ? error.message : String(error),
2370
+ hint: "Make sure the Desktop Bridge plugin is running. baseComponentId/componentIds must reference COMPONENT nodes that are not already inside a component set. Node IDs are session-specific — re-search components if they may be stale.",
2371
+ }),
2372
+ },
2373
+ ],
2374
+ isError: true,
2375
+ };
2376
+ }
2377
+ });
2109
2378
  // Tool: Lint Design for accessibility and quality issues
2110
2379
  server.tool("figma_lint_design", "Run comprehensive accessibility (WCAG 2.2) and design quality checks on the current page or a specific node tree. " +
2111
2380
  "WCAG conformance checks (10 rules): color contrast (AA), non-text contrast (1.4.11), color-only differentiation (1.4.1), " +