@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
@@ -5,8 +5,11 @@
5
5
  // which then forwards messages to the MCP server over the WebSocket bridge.
6
6
 
7
7
  // Plugin version — sent in FILE_INFO for server-side version compatibility checks.
8
- // The server compares this against its own version to detect stale cached plugins.
9
- var PLUGIN_VERSION = '1.32.1'; // Kept in sync with package.json by scripts/release.sh see issue #62.
8
+ // The server compares this against the version of the plugin files IT ships to
9
+ // detect stale cached plugins. Bumped by scripts/release.sh ONLY when plugin files
10
+ // change (see issue #62); server-only releases leave it alone, so it may lag
11
+ // package.json — that's intentional, not drift.
12
+ var PLUGIN_VERSION = '1.34.0'; // Last release in which plugin files changed.
10
13
 
11
14
  console.log('🌉 [Desktop Bridge] Plugin loaded (v' + PLUGIN_VERSION + ')');
12
15
 
@@ -148,6 +151,23 @@ var __stickyColors = {
148
151
  }
149
152
  })();
150
153
 
154
+ // Restore persisted cloud pairing config (stored via STORE_CLOUD_CONFIG) and
155
+ // push it to the UI so cloud users don't lose their pairing on plugin reopen.
156
+ // Fire-and-forget: never blocks the FILE_INFO/VARIABLES_DATA pushes above.
157
+ (function() {
158
+ figma.clientStorage.getAsync('cloudConfig')
159
+ .then(function(stored) {
160
+ // Skip if nothing stored or the shape is unusable (e.g., cleared config)
161
+ if (!stored || !stored.code) return;
162
+ figma.ui.postMessage({ type: 'CLOUD_CONFIG_RESTORED', config: stored });
163
+ console.log('🌉 [Desktop Bridge] Restored cloud config from clientStorage');
164
+ })
165
+ .catch(function(error) {
166
+ // clientStorage can throw — non-critical, just log
167
+ console.warn('🌉 [Desktop Bridge] Could not restore cloud config:', error && error.message ? error.message : String(error));
168
+ });
169
+ })();
170
+
151
171
  // Helper function to serialize a variable for response
152
172
  function serializeVariable(v) {
153
173
  return {
@@ -356,6 +376,32 @@ async function loadFontsForNode(node) {
356
376
  }
357
377
  }
358
378
 
379
+ // Resolve a caller-supplied component property name against the instance's
380
+ // actual property keys. TEXT/BOOLEAN/INSTANCE_SWAP property names carry a
381
+ // #nodeId suffix (e.g. "title#2605:17"), so a bare "title" from the caller
382
+ // must be mapped to the suffixed key. Resolution tiers:
383
+ // 1. Exact match (covers VARIANT props and fully-suffixed names)
384
+ // 2. Base name before '#' matches exactly
385
+ // 3. Case-insensitive base-name match
386
+ // Returns the resolved key, or null if nothing matches.
387
+ function resolvePropertyName(currentProps, name) {
388
+ if (!currentProps || !name) return null;
389
+ if (currentProps[name] !== undefined) return name;
390
+ var existingProp;
391
+ for (existingProp in currentProps) {
392
+ if (currentProps.hasOwnProperty(existingProp) && existingProp.split('#')[0] === name) {
393
+ return existingProp;
394
+ }
395
+ }
396
+ var lowerName = String(name).toLowerCase();
397
+ for (existingProp in currentProps) {
398
+ if (currentProps.hasOwnProperty(existingProp) && existingProp.split('#')[0].toLowerCase() === lowerName) {
399
+ return existingProp;
400
+ }
401
+ }
402
+ return null;
403
+ }
404
+
359
405
  // Listen for requests from UI (e.g., component data requests, write operations)
360
406
  figma.ui.onmessage = async (msg) => {
361
407
 
@@ -1904,12 +1950,19 @@ figma.ui.onmessage = async (msg) => {
1904
1950
  // Track failures so they surface in the result instead of failing silently.
1905
1951
  var overrideWarnings = [];
1906
1952
 
1907
- // Apply property overrides
1953
+ // Apply property overrides. Property keys on the instance carry #nodeId
1954
+ // suffixes ("title#2605:17"), so resolve the caller's bare name first —
1955
+ // otherwise setProperties throws "Could not find a component property".
1908
1956
  if (msg.overrides) {
1957
+ var instanceProps = {};
1958
+ try { instanceProps = instance.componentProperties || {}; } catch (e) { instanceProps = {}; }
1909
1959
  for (var propName in msg.overrides) {
1910
1960
  if (msg.overrides.hasOwnProperty(propName)) {
1961
+ var resolvedName = resolvePropertyName(instanceProps, propName);
1911
1962
  try {
1912
- instance.setProperties({ [propName]: msg.overrides[propName] });
1963
+ var propsObj = {};
1964
+ propsObj[resolvedName || propName] = msg.overrides[propName];
1965
+ instance.setProperties(propsObj);
1913
1966
  } catch (propError) {
1914
1967
  var pMsg = propError && propError.message ? propError.message : String(propError);
1915
1968
  console.warn('🌉 [Desktop Bridge] Could not set property ' + propName + ':', pMsg);
@@ -1935,6 +1988,8 @@ figma.ui.onmessage = async (msg) => {
1935
1988
  var parent = await figma.getNodeByIdAsync(msg.parentId);
1936
1989
  if (parent && 'appendChild' in parent) {
1937
1990
  parent.appendChild(instance);
1991
+ } else {
1992
+ overrideWarnings.push('parentId "' + msg.parentId + '" not found or cannot accept children; instance placed on current page');
1938
1993
  }
1939
1994
  }
1940
1995
 
@@ -1967,6 +2022,293 @@ figma.ui.onmessage = async (msg) => {
1967
2022
  }
1968
2023
  }
1969
2024
 
2025
+ // ============================================================================
2026
+ // CREATE_COMPONENT_SET - Build a component set with variants in one call
2027
+ // Two modes:
2028
+ // A) baseComponentId + properties: clone the base for every combination of
2029
+ // the property axes ({State:[default,hover], Size:[sm,lg]} → 4 variants),
2030
+ // rename each to "Prop=Value, Prop=Value", combineAsVariants.
2031
+ // The base component itself BECOMES the first variant (same node ID),
2032
+ // so existing instances of the base survive as instances of that variant.
2033
+ // B) componentIds (+ optional variantProperties): combine existing local
2034
+ // COMPONENT nodes as variants, optionally renaming them first.
2035
+ // Variant properties are derived by Figma from the "Prop=Value" names, and
2036
+ // componentPropertyDefinitions live on the resulting SET (not the variants).
2037
+ // No text is written here, so no font loading is needed (clone() copies text
2038
+ // without touching characters).
2039
+ // ============================================================================
2040
+ else if (msg.type === 'CREATE_COMPONENT_SET') {
2041
+ try {
2042
+ console.log('🌉 [Desktop Bridge] Creating component set:', msg.name || msg.baseComponentId || (msg.componentIds && msg.componentIds.length + ' components'));
2043
+
2044
+ var MAX_VARIANTS = 100;
2045
+ var setWarnings = [];
2046
+ // Rollback state — if anything throws before combineAsVariants succeeds,
2047
+ // remove Mode A clones, restore the base's name, AND restore every
2048
+ // Mode B component the handler renamed, so no partial artifacts remain.
2049
+ // (Previously only Mode A was rolled back: a throw from the
2050
+ // duplicate-combo check or combineAsVariants left the user's standalone
2051
+ // components permanently renamed with no set created.)
2052
+ var createdClones = [];
2053
+ var renamedBase = null;
2054
+ var originalBaseName = null;
2055
+ var renamedNodes = []; // Mode B: [{ node, originalName }]
2056
+ var combineDone = false;
2057
+
2058
+ function rollbackComponentSetWork() {
2059
+ for (var rc = 0; rc < createdClones.length; rc++) {
2060
+ try { createdClones[rc].remove(); } catch (e) {}
2061
+ }
2062
+ createdClones = [];
2063
+ if (renamedBase && originalBaseName !== null) {
2064
+ try { renamedBase.name = originalBaseName; } catch (e) {}
2065
+ renamedBase = null;
2066
+ }
2067
+ for (var rn = 0; rn < renamedNodes.length; rn++) {
2068
+ try { renamedNodes[rn].node.name = renamedNodes[rn].originalName; } catch (e) {}
2069
+ }
2070
+ renamedNodes = [];
2071
+ }
2072
+
2073
+ // Build "Prop=Value, Prop=Value" from an axis-ordered property map
2074
+ function comboName(combo, axisOrder) {
2075
+ var parts = [];
2076
+ for (var a = 0; a < axisOrder.length; a++) {
2077
+ parts.push(axisOrder[a] + '=' + combo[axisOrder[a]]);
2078
+ }
2079
+ return parts.join(', ');
2080
+ }
2081
+
2082
+ var variantNodes = [];
2083
+
2084
+ if (msg.baseComponentId) {
2085
+ // ---- Mode A: generate variants from a base component ----
2086
+ if (!msg.properties || Object.keys(msg.properties).length === 0) {
2087
+ throw new Error('properties is required with baseComponentId. Example: { "State": ["default", "hover"], "Size": ["sm", "lg"] }');
2088
+ }
2089
+
2090
+ var base = await figma.getNodeByIdAsync(msg.baseComponentId);
2091
+ if (!base) throw new Error('Base component not found: ' + msg.baseComponentId + '. NodeIds are session-specific — re-search components to get fresh IDs.');
2092
+ if (base.type !== 'COMPONENT') {
2093
+ throw new Error('baseComponentId must be a COMPONENT node, got ' + base.type + '. To convert a frame, use figma_execute with figma.createComponentFromNode(frame) first.');
2094
+ }
2095
+ if (base.parent && base.parent.type === 'COMPONENT_SET') {
2096
+ throw new Error('Base component is already a variant inside component set "' + base.parent.name + '". Use a standalone component.');
2097
+ }
2098
+
2099
+ // Validate axes and build the cartesian product (last axis varies fastest,
2100
+ // matching Figma's convention of State as the final property)
2101
+ var axisOrder = [];
2102
+ var axisValues = {};
2103
+ for (var axis in msg.properties) {
2104
+ if (msg.properties.hasOwnProperty(axis)) {
2105
+ var vals = msg.properties[axis];
2106
+ if (!vals || !vals.length) throw new Error('Property axis "' + axis + '" has no values');
2107
+ var uniq = {};
2108
+ for (var vv = 0; vv < vals.length; vv++) {
2109
+ var sval = String(vals[vv]);
2110
+ if (sval.indexOf('=') !== -1 || sval.indexOf(',') !== -1) {
2111
+ throw new Error('Variant value "' + sval + '" (axis "' + axis + '") must not contain "=" or "," — Figma parses variant names on those characters');
2112
+ }
2113
+ if (uniq[sval]) throw new Error('Duplicate value "' + sval + '" in axis "' + axis + '"');
2114
+ uniq[sval] = true;
2115
+ }
2116
+ if (axis.indexOf('=') !== -1 || axis.indexOf(',') !== -1) {
2117
+ throw new Error('Property name "' + axis + '" must not contain "=" or ","');
2118
+ }
2119
+ axisOrder.push(axis);
2120
+ axisValues[axis] = vals.map(String);
2121
+ }
2122
+ }
2123
+
2124
+ var combos = [{}];
2125
+ for (var ai = 0; ai < axisOrder.length; ai++) {
2126
+ var nextCombos = [];
2127
+ var axisName = axisOrder[ai];
2128
+ for (var ci = 0; ci < combos.length; ci++) {
2129
+ for (var vi2 = 0; vi2 < axisValues[axisName].length; vi2++) {
2130
+ var extended = {};
2131
+ for (var k in combos[ci]) { if (combos[ci].hasOwnProperty(k)) extended[k] = combos[ci][k]; }
2132
+ extended[axisName] = axisValues[axisName][vi2];
2133
+ nextCombos.push(extended);
2134
+ }
2135
+ }
2136
+ combos = nextCombos;
2137
+ }
2138
+
2139
+ if (combos.length > MAX_VARIANTS) {
2140
+ throw new Error('Requested ' + combos.length + ' variants — capped at ' + MAX_VARIANTS + '. Split into multiple sets or reduce axis values.');
2141
+ }
2142
+
2143
+ // Base becomes the first combination; clones fill in the rest.
2144
+ // Stack clones vertically so nothing overlaps even without autoArrange.
2145
+ var baseX = base.x;
2146
+ var baseY = base.y;
2147
+ var stepY = Math.ceil(base.height) + 40;
2148
+ originalBaseName = base.name;
2149
+ renamedBase = base;
2150
+ base.name = comboName(combos[0], axisOrder);
2151
+ variantNodes.push(base);
2152
+ for (var ci2 = 1; ci2 < combos.length; ci2++) {
2153
+ var clone = base.clone();
2154
+ clone.name = comboName(combos[ci2], axisOrder);
2155
+ clone.x = baseX;
2156
+ clone.y = baseY + ci2 * stepY;
2157
+ createdClones.push(clone);
2158
+ variantNodes.push(clone);
2159
+ }
2160
+ } else if (msg.componentIds && msg.componentIds.length) {
2161
+ // ---- Mode B: combine existing components ----
2162
+ if (msg.componentIds.length > MAX_VARIANTS) {
2163
+ throw new Error('Requested ' + msg.componentIds.length + ' variants — capped at ' + MAX_VARIANTS + '.');
2164
+ }
2165
+ if (msg.variantProperties && msg.variantProperties.length !== msg.componentIds.length) {
2166
+ throw new Error('variantProperties length (' + msg.variantProperties.length + ') must match componentIds length (' + msg.componentIds.length + ')');
2167
+ }
2168
+
2169
+ for (var ni = 0; ni < msg.componentIds.length; ni++) {
2170
+ var node = await figma.getNodeByIdAsync(msg.componentIds[ni]);
2171
+ if (!node) throw new Error('Component not found: ' + msg.componentIds[ni] + '. NodeIds are session-specific — re-search components to get fresh IDs.');
2172
+ if (node.type !== 'COMPONENT') {
2173
+ throw new Error('Node ' + msg.componentIds[ni] + ' is a ' + node.type + ', not a COMPONENT. All componentIds must reference COMPONENT nodes.');
2174
+ }
2175
+ if (node.parent && node.parent.type === 'COMPONENT_SET') {
2176
+ throw new Error('Component "' + node.name + '" (' + node.id + ') is already a variant in set "' + node.parent.name + '". Components can only belong to one set.');
2177
+ }
2178
+ if (msg.variantProperties) {
2179
+ var props = msg.variantProperties[ni];
2180
+ var propKeys = Object.keys(props);
2181
+ if (!propKeys.length) throw new Error('variantProperties[' + ni + '] is empty — each entry needs at least one property (e.g. { "State": "hover" })');
2182
+ // Same '='/',' validation as Mode A — a value like "hover,Size=lg"
2183
+ // would silently create a bogus axis when Figma parses the name.
2184
+ for (var pk = 0; pk < propKeys.length; pk++) {
2185
+ var propName = propKeys[pk];
2186
+ var propValue = String(props[propName]);
2187
+ if (propName.indexOf('=') !== -1 || propName.indexOf(',') !== -1) {
2188
+ throw new Error('Property name "' + propName + '" (variantProperties[' + ni + ']) must not contain "=" or "," — Figma parses variant names on those characters');
2189
+ }
2190
+ if (propValue.indexOf('=') !== -1 || propValue.indexOf(',') !== -1) {
2191
+ throw new Error('Variant value "' + propValue + '" (property "' + propName + '", variantProperties[' + ni + ']) must not contain "=" or "," — Figma parses variant names on those characters');
2192
+ }
2193
+ }
2194
+ // Record the rename so the rollback path can restore it.
2195
+ renamedNodes.push({ node: node, originalName: node.name });
2196
+ variantNodes.push(node);
2197
+ node.name = comboName(props, propKeys);
2198
+ } else {
2199
+ variantNodes.push(node);
2200
+ if (node.name.indexOf('=') === -1) {
2201
+ setWarnings.push('Component "' + node.name + '" is not named "Prop=Value" — Figma will file it under "Property 1=' + node.name + '". Pass variantProperties to control naming.');
2202
+ }
2203
+ }
2204
+ }
2205
+
2206
+ // Duplicate variant names create an invalid (conflicting) set — fail early
2207
+ var nameSeen = {};
2208
+ for (var dn = 0; dn < variantNodes.length; dn++) {
2209
+ if (nameSeen[variantNodes[dn].name]) {
2210
+ throw new Error('Two variants would have identical properties: "' + variantNodes[dn].name + '". Each variant needs a unique property combination.');
2211
+ }
2212
+ nameSeen[variantNodes[dn].name] = true;
2213
+ }
2214
+ } else {
2215
+ throw new Error('Provide either baseComponentId + properties (generate variants) or componentIds (combine existing components).');
2216
+ }
2217
+
2218
+ // Resolve the parent — combineAsVariants REQUIRES a parent, and the set is
2219
+ // created inside it. Fall back to the current page.
2220
+ var setParent = figma.currentPage;
2221
+ if (msg.parentId) {
2222
+ var requestedParent = await figma.getNodeByIdAsync(msg.parentId);
2223
+ if (requestedParent && typeof requestedParent.appendChild === 'function' && requestedParent.type !== 'COMPONENT' && requestedParent.type !== 'COMPONENT_SET' && requestedParent.type !== 'INSTANCE') {
2224
+ setParent = requestedParent;
2225
+ } else {
2226
+ setWarnings.push('parentId "' + msg.parentId + '" not found or cannot contain a component set; created on the current page instead');
2227
+ }
2228
+ }
2229
+
2230
+ // Housekeeping on failure happens in the outer catch (gated on
2231
+ // !combineDone) — it restores Mode A clones/base AND Mode B renames.
2232
+ var componentSet = figma.combineAsVariants(variantNodes, setParent);
2233
+ combineDone = true;
2234
+ if (msg.name) componentSet.name = msg.name;
2235
+ if (msg.position) {
2236
+ componentSet.x = msg.position.x || 0;
2237
+ componentSet.y = msg.position.y || 0;
2238
+ }
2239
+
2240
+ // componentPropertyDefinitions live on the SET (not the variants) —
2241
+ // read them back so callers see the axes Figma actually derived.
2242
+ var propertyDefinitions = {};
2243
+ try {
2244
+ var defs = componentSet.componentPropertyDefinitions;
2245
+ for (var defName in defs) {
2246
+ if (defs.hasOwnProperty(defName)) {
2247
+ propertyDefinitions[defName] = {
2248
+ type: defs[defName].type,
2249
+ defaultValue: defs[defName].defaultValue,
2250
+ variantOptions: defs[defName].variantOptions || []
2251
+ };
2252
+ }
2253
+ }
2254
+ } catch (defError) {
2255
+ setWarnings.push('Could not read componentPropertyDefinitions: ' + (defError && defError.message ? defError.message : String(defError)));
2256
+ }
2257
+
2258
+ var variantSummaries = [];
2259
+ for (var vs = 0; vs < componentSet.children.length; vs++) {
2260
+ var child = componentSet.children[vs];
2261
+ if (child.type === 'COMPONENT') {
2262
+ // Include each variant's key — instantiation uses VARIANT keys, not the set key
2263
+ variantSummaries.push({ id: child.id, name: child.name, key: child.key });
2264
+ }
2265
+ }
2266
+
2267
+ figma.currentPage.selection = [componentSet];
2268
+ figma.viewport.scrollAndZoomIntoView([componentSet]);
2269
+
2270
+ console.log('🌉 [Desktop Bridge] Component set created:', componentSet.id, 'with', variantSummaries.length, 'variants');
2271
+
2272
+ // Everything rides in `data` — ui.html's handleResult only forwards
2273
+ // whitelisted top-level fields, and msg.data is on that whitelist.
2274
+ figma.ui.postMessage({
2275
+ type: 'CREATE_COMPONENT_SET_RESULT',
2276
+ requestId: msg.requestId,
2277
+ success: true,
2278
+ data: {
2279
+ componentSet: {
2280
+ id: componentSet.id,
2281
+ name: componentSet.name,
2282
+ key: componentSet.key,
2283
+ x: componentSet.x,
2284
+ y: componentSet.y,
2285
+ width: componentSet.width,
2286
+ height: componentSet.height,
2287
+ parentId: componentSet.parent ? componentSet.parent.id : null
2288
+ },
2289
+ variantCount: variantSummaries.length,
2290
+ variants: variantSummaries,
2291
+ propertyDefinitions: propertyDefinitions,
2292
+ warnings: setWarnings.length ? setWarnings : undefined
2293
+ }
2294
+ });
2295
+
2296
+ } catch (error) {
2297
+ var csErrorMsg = error && error.message ? error.message : String(error);
2298
+ console.error('🌉 [Desktop Bridge] Create component set error:', csErrorMsg);
2299
+ // Undo partial work unless the set was actually created (after
2300
+ // combineAsVariants succeeds the clones/renames live inside the set —
2301
+ // never touch them then).
2302
+ try { if (!combineDone) rollbackComponentSetWork(); } catch (e) {}
2303
+ figma.ui.postMessage({
2304
+ type: 'CREATE_COMPONENT_SET_RESULT',
2305
+ requestId: msg.requestId,
2306
+ success: false,
2307
+ error: csErrorMsg
2308
+ });
2309
+ }
2310
+ }
2311
+
1970
2312
  // ============================================================================
1971
2313
  // SET_NODE_DESCRIPTION - Set description on component/style
1972
2314
  // ============================================================================
@@ -2556,16 +2898,25 @@ figma.ui.onmessage = async (msg) => {
2556
2898
  var nodeIds = msg.nodeIds || (msg.nodeId ? [msg.nodeId] : []);
2557
2899
  var updatedCount = 0;
2558
2900
  var updatedNodes = [];
2901
+ var skipWarnings = [];
2559
2902
 
2560
2903
  for (var i = 0; i < nodeIds.length; i++) {
2561
2904
  var node = await figma.getNodeByIdAsync(nodeIds[i]);
2562
- if (node && 'fills' in node) {
2905
+ if (!node) {
2906
+ skipWarnings.push('node "' + nodeIds[i] + '" not found');
2907
+ } else if (!('fills' in node)) {
2908
+ skipWarnings.push('node "' + nodeIds[i] + '" (' + node.type + ') does not support fills');
2909
+ } else {
2563
2910
  node.fills = [fill];
2564
2911
  updatedCount++;
2565
2912
  updatedNodes.push({ id: node.id, name: node.name });
2566
2913
  }
2567
2914
  }
2568
2915
 
2916
+ if (updatedCount === 0 && nodeIds.length > 0) {
2917
+ throw new Error('Image fill applied to 0 node(s): ' + skipWarnings.join('; '));
2918
+ }
2919
+
2569
2920
  console.log('🌉 [Desktop Bridge] Image fill applied to', updatedCount, 'node(s), hash:', imageHash);
2570
2921
 
2571
2922
  figma.ui.postMessage({
@@ -2574,7 +2925,8 @@ figma.ui.onmessage = async (msg) => {
2574
2925
  success: true,
2575
2926
  imageHash: imageHash,
2576
2927
  updatedCount: updatedCount,
2577
- nodes: updatedNodes
2928
+ nodes: updatedNodes,
2929
+ warnings: skipWarnings.length ? skipWarnings : undefined
2578
2930
  });
2579
2931
 
2580
2932
  } catch (error) {
@@ -2968,8 +3320,10 @@ figma.ui.onmessage = async (msg) => {
2968
3320
  if (props.name) newNode.name = props.name;
2969
3321
  if (props.x !== undefined) newNode.x = props.x;
2970
3322
  if (props.y !== undefined) newNode.y = props.y;
2971
- if (props.width !== undefined && props.height !== undefined) {
2972
- newNode.resize(props.width, props.height);
3323
+ if (props.width !== undefined || props.height !== undefined) {
3324
+ var resizeWidth = props.width !== undefined ? props.width : newNode.width;
3325
+ var resizeHeight = props.height !== undefined ? props.height : newNode.height;
3326
+ newNode.resize(resizeWidth, resizeHeight);
2973
3327
  }
2974
3328
 
2975
3329
  // Apply fills if specified
@@ -3293,30 +3647,23 @@ figma.ui.onmessage = async (msg) => {
3293
3647
  // VARIANT properties use just "PropertyName"
3294
3648
  var propsToSet = {};
3295
3649
  var propUpdates = msg.properties || {};
3650
+ var propWarnings = [];
3651
+ var hasTextProp = false;
3296
3652
 
3297
3653
  for (var propName in propUpdates) {
3654
+ if (!propUpdates.hasOwnProperty(propName)) continue;
3298
3655
  var newValue = propUpdates[propName];
3656
+ var resolvedName = resolvePropertyName(currentProps, propName);
3299
3657
 
3300
- // Check if this exact property name exists
3301
- if (currentProps[propName] !== undefined) {
3302
- propsToSet[propName] = newValue;
3303
- console.log('🌉 [Desktop Bridge] Setting property:', propName, '=', newValue);
3304
- } else {
3305
- // Try to find a matching property with a suffix (for TEXT/BOOLEAN/INSTANCE_SWAP)
3306
- var foundMatch = false;
3307
- for (var existingProp in currentProps) {
3308
- // Check if this is the base property name with a node ID suffix
3309
- if (existingProp.startsWith(propName + '#')) {
3310
- propsToSet[existingProp] = newValue;
3311
- console.log('🌉 [Desktop Bridge] Found suffixed property:', existingProp, '=', newValue);
3312
- foundMatch = true;
3313
- break;
3314
- }
3315
- }
3316
-
3317
- if (!foundMatch) {
3318
- console.warn('🌉 [Desktop Bridge] Property not found:', propName, '- Available:', Object.keys(currentProps).join(', '));
3658
+ if (resolvedName !== null) {
3659
+ propsToSet[resolvedName] = newValue;
3660
+ if (currentProps[resolvedName] && currentProps[resolvedName].type === 'TEXT') {
3661
+ hasTextProp = true;
3319
3662
  }
3663
+ console.log('🌉 [Desktop Bridge] Setting property:', resolvedName, '=', newValue);
3664
+ } else {
3665
+ console.warn('🌉 [Desktop Bridge] Property not found:', propName, '- Available:', Object.keys(currentProps).join(', '));
3666
+ propWarnings.push('property "' + propName + '" not found; available: ' + Object.keys(currentProps).join(', '));
3320
3667
  }
3321
3668
  }
3322
3669
 
@@ -3324,6 +3671,12 @@ figma.ui.onmessage = async (msg) => {
3324
3671
  throw new Error('No valid properties to set. Available properties: ' + Object.keys(currentProps).join(', '));
3325
3672
  }
3326
3673
 
3674
+ // Pre-load fonts before applying TEXT-property values. Mutating text
3675
+ // content in dynamic-page mode throws unless fonts are already loaded.
3676
+ if (hasTextProp) {
3677
+ await loadFontsForNode(node);
3678
+ }
3679
+
3327
3680
  // Apply the properties
3328
3681
  node.setProperties(propsToSet);
3329
3682
 
@@ -3348,7 +3701,8 @@ figma.ui.onmessage = async (msg) => {
3348
3701
  };
3349
3702
  return acc;
3350
3703
  }, {})
3351
- }
3704
+ },
3705
+ warnings: propWarnings.length ? propWarnings : undefined
3352
3706
  });
3353
3707
 
3354
3708
  } catch (error) {