@mp3wizard/figma-console-mcp 1.29.3 → 1.31.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.
@@ -6,12 +6,12 @@
6
6
 
7
7
  // Plugin version — sent in FILE_INFO for server-side version compatibility checks.
8
8
  // The server compares this against its own version to detect stale cached plugins.
9
- var PLUGIN_VERSION = '1.29.2'; // Kept in sync with package.json by scripts/release.sh — see issue #62.
9
+ var PLUGIN_VERSION = '1.31.0'; // Kept in sync with package.json by scripts/release.sh — see issue #62.
10
10
 
11
11
  console.log('🌉 [Desktop Bridge] Plugin loaded (v' + PLUGIN_VERSION + ')');
12
12
 
13
13
  // Show minimal UI - compact status indicator
14
- figma.showUI(__html__, { width: 180, height: 50, visible: true, themeColors: true });
14
+ figma.showUI(__html__, { width: 240, height: 40, visible: true, themeColors: true });
15
15
 
16
16
  // ============================================================================
17
17
  // CONSOLE CAPTURE — Intercept console.* in the QuickJS sandbox and forward
@@ -218,6 +218,80 @@ function hexToFigmaRGB(hex) {
218
218
  return { r: r, g: g, b: b, a: a };
219
219
  }
220
220
 
221
+ // Build the ordered list of font-style names to try for a requested style.
222
+ // Figma style names are exact and space-sensitive: Inter's semibold is
223
+ // "Semi Bold" (with a space), not "SemiBold". We try the value as-is first
224
+ // (some families legitimately use no-space names), then a space-normalized
225
+ // variant, then a space-collapsed variant.
226
+ function normalizeFontStyleVariants(style) {
227
+ var variants = [];
228
+ function push(v) { if (v && variants.indexOf(v) === -1) variants.push(v); }
229
+ push(style);
230
+ push(String(style).replace(/([a-z])([A-Z])/g, '$1 $2')); // "SemiBold" -> "Semi Bold"
231
+ push(String(style).replace(/\s+/g, '')); // "Semi Bold" -> "SemiBold"
232
+ return variants;
233
+ }
234
+
235
+ // Load a font, tolerating common style-name variants. A wrong style name
236
+ // otherwise fails (often silently), leaving wrong typography with no error.
237
+ // Falls back to "Regular" so a bad weight degrades gracefully, and throws a
238
+ // clear, actionable error only if nothing loads.
239
+ async function loadFontWithFallback(family, requestedStyle) {
240
+ var style = requestedStyle || 'Regular';
241
+ var attempts = normalizeFontStyleVariants(style);
242
+ for (var i = 0; i < attempts.length; i++) {
243
+ try {
244
+ var fontName = { family: family, style: attempts[i] };
245
+ await figma.loadFontAsync(fontName);
246
+ return fontName;
247
+ } catch (e) { /* try next variant */ }
248
+ }
249
+ try {
250
+ var fallback = { family: family, style: 'Regular' };
251
+ await figma.loadFontAsync(fallback);
252
+ return fallback;
253
+ } catch (e) {
254
+ throw new Error('Could not load font "' + family + ' ' + style + '". Tried: ' +
255
+ attempts.join(', ') + ', Regular. Check the family name and that the weight exists ' +
256
+ '(Figma styles are space-sensitive, e.g. "Semi Bold" not "SemiBold").');
257
+ }
258
+ }
259
+
260
+ // Pre-load every font used by a node's text descendants in ONE pass. Mutating
261
+ // a text node in dynamic-page mode throws unless its font is loaded first;
262
+ // loading per-node in a loop is also what causes timeouts at scale.
263
+ async function loadFontsForNode(node) {
264
+ if (!node) return;
265
+ var textNodes = [];
266
+ if (node.type === 'TEXT') {
267
+ textNodes = [node];
268
+ } else if (typeof node.findAllWithCriteria === 'function') {
269
+ try { textNodes = node.findAllWithCriteria({ types: ['TEXT'] }); }
270
+ catch (e) { textNodes = []; }
271
+ } else if (typeof node.findAll === 'function') {
272
+ textNodes = node.findAll(function(n) { return n.type === 'TEXT'; });
273
+ }
274
+ var seen = {};
275
+ var fonts = [];
276
+ for (var i = 0; i < textNodes.length; i++) {
277
+ var tn = textNodes[i];
278
+ var names = [];
279
+ if (tn.fontName === figma.mixed) {
280
+ try { names = tn.getRangeAllFontNames(0, tn.characters.length); }
281
+ catch (e) { names = []; }
282
+ } else if (tn.fontName) {
283
+ names = [tn.fontName];
284
+ }
285
+ for (var j = 0; j < names.length; j++) {
286
+ var key = names[j].family + '||' + names[j].style;
287
+ if (!seen[key]) { seen[key] = true; fonts.push(names[j]); }
288
+ }
289
+ }
290
+ for (var k = 0; k < fonts.length; k++) {
291
+ try { await figma.loadFontAsync(fonts[k]); } catch (e) { /* skip unavailable */ }
292
+ }
293
+ }
294
+
221
295
  // Listen for requests from UI (e.g., component data requests, write operations)
222
296
  figma.ui.onmessage = async (msg) => {
223
297
 
@@ -1748,6 +1822,15 @@ figma.ui.onmessage = async (msg) => {
1748
1822
  instance.resize(msg.size.width, msg.size.height);
1749
1823
  }
1750
1824
 
1825
+ // Pre-load fonts for the instance's text nodes BEFORE applying overrides.
1826
+ // Text-property overrides mutate text content, which throws in dynamic-page
1827
+ // mode unless the font is already loaded. Loading once up front also avoids
1828
+ // the per-node timeouts seen when fonts are loaded inside a loop.
1829
+ await loadFontsForNode(instance);
1830
+
1831
+ // Track failures so they surface in the result instead of failing silently.
1832
+ var overrideWarnings = [];
1833
+
1751
1834
  // Apply property overrides
1752
1835
  if (msg.overrides) {
1753
1836
  for (var propName in msg.overrides) {
@@ -1755,7 +1838,9 @@ figma.ui.onmessage = async (msg) => {
1755
1838
  try {
1756
1839
  instance.setProperties({ [propName]: msg.overrides[propName] });
1757
1840
  } catch (propError) {
1758
- console.warn('🌉 [Desktop Bridge] Could not set property ' + propName + ':', propError.message);
1841
+ var pMsg = propError && propError.message ? propError.message : String(propError);
1842
+ console.warn('🌉 [Desktop Bridge] Could not set property ' + propName + ':', pMsg);
1843
+ overrideWarnings.push('override "' + propName + '" failed: ' + pMsg);
1759
1844
  }
1760
1845
  }
1761
1846
  }
@@ -1766,7 +1851,9 @@ figma.ui.onmessage = async (msg) => {
1766
1851
  try {
1767
1852
  instance.setProperties(msg.variant);
1768
1853
  } catch (variantError) {
1769
- console.warn('🌉 [Desktop Bridge] Could not set variant:', variantError.message);
1854
+ var vMsg = variantError && variantError.message ? variantError.message : String(variantError);
1855
+ console.warn('🌉 [Desktop Bridge] Could not set variant:', vMsg);
1856
+ overrideWarnings.push('variant selection failed: ' + vMsg);
1770
1857
  }
1771
1858
  }
1772
1859
 
@@ -1791,7 +1878,8 @@ figma.ui.onmessage = async (msg) => {
1791
1878
  y: instance.y,
1792
1879
  width: instance.width,
1793
1880
  height: instance.height
1794
- }
1881
+ },
1882
+ warnings: overrideWarnings.length ? overrideWarnings : undefined
1795
1883
  });
1796
1884
 
1797
1885
  } catch (error) {
@@ -2320,19 +2408,32 @@ figma.ui.onmessage = async (msg) => {
2320
2408
  throw new Error('Node type ' + node.type + ' does not support fills');
2321
2409
  }
2322
2410
 
2323
- // Process fills - convert hex colors if needed
2324
- var processedFills = msg.fills.map(function(fill) {
2325
- if (fill.type === 'SOLID' && typeof fill.color === 'string') {
2326
- // Convert hex to RGB
2327
- var rgb = hexToFigmaRGB(fill.color);
2328
- return {
2411
+ // Process fills - convert hex colors, and optionally bind a color variable.
2412
+ // Color variables bind at the PAINT level (not the node level), so we build
2413
+ // the solid paint then attach the binding via setBoundVariableForPaint.
2414
+ var processedFills = [];
2415
+ for (var fi = 0; fi < msg.fills.length; fi++) {
2416
+ var fill = msg.fills[fi];
2417
+ if (fill.type === 'SOLID') {
2418
+ var baseHex = typeof fill.color === 'string' ? fill.color : '#000000';
2419
+ var rgb = hexToFigmaRGB(baseHex);
2420
+ var paint = {
2329
2421
  type: 'SOLID',
2330
2422
  color: { r: rgb.r, g: rgb.g, b: rgb.b },
2331
2423
  opacity: rgb.a !== undefined ? rgb.a : (fill.opacity !== undefined ? fill.opacity : 1)
2332
2424
  };
2425
+ if (fill.variableId) {
2426
+ var fillVar = await figma.variables.getVariableByIdAsync(fill.variableId);
2427
+ if (!fillVar) {
2428
+ throw new Error('Fill variable not found: "' + fill.variableId + '". Pass a local variable id from figma_get_variables (e.g. "VariableID:1:23"). Library variables must be imported first via figma_import_library_variable.');
2429
+ }
2430
+ paint = figma.variables.setBoundVariableForPaint(paint, 'color', fillVar);
2431
+ }
2432
+ processedFills.push(paint);
2433
+ } else {
2434
+ processedFills.push(fill);
2333
2435
  }
2334
- return fill;
2335
- });
2436
+ }
2336
2437
 
2337
2438
  node.fills = processedFills;
2338
2439
 
@@ -2431,18 +2532,31 @@ figma.ui.onmessage = async (msg) => {
2431
2532
  throw new Error('Node type ' + node.type + ' does not support strokes');
2432
2533
  }
2433
2534
 
2434
- // Process strokes - convert hex colors if needed
2435
- var processedStrokes = msg.strokes.map(function(stroke) {
2436
- if (stroke.type === 'SOLID' && typeof stroke.color === 'string') {
2437
- var rgb = hexToFigmaRGB(stroke.color);
2438
- return {
2535
+ // Process strokes - convert hex colors, and optionally bind a color variable
2536
+ // (paint-level binding, same as fills).
2537
+ var processedStrokes = [];
2538
+ for (var sti = 0; sti < msg.strokes.length; sti++) {
2539
+ var stroke = msg.strokes[sti];
2540
+ if (stroke.type === 'SOLID') {
2541
+ var sBaseHex = typeof stroke.color === 'string' ? stroke.color : '#000000';
2542
+ var srgb = hexToFigmaRGB(sBaseHex);
2543
+ var spaint = {
2439
2544
  type: 'SOLID',
2440
- color: { r: rgb.r, g: rgb.g, b: rgb.b },
2441
- opacity: rgb.a !== undefined ? rgb.a : (stroke.opacity !== undefined ? stroke.opacity : 1)
2545
+ color: { r: srgb.r, g: srgb.g, b: srgb.b },
2546
+ opacity: srgb.a !== undefined ? srgb.a : (stroke.opacity !== undefined ? stroke.opacity : 1)
2442
2547
  };
2548
+ if (stroke.variableId) {
2549
+ var strokeVar = await figma.variables.getVariableByIdAsync(stroke.variableId);
2550
+ if (!strokeVar) {
2551
+ throw new Error('Stroke variable not found: "' + stroke.variableId + '". Pass a local variable id from figma_get_variables. Library variables must be imported first via figma_import_library_variable.');
2552
+ }
2553
+ spaint = figma.variables.setBoundVariableForPaint(spaint, 'color', strokeVar);
2554
+ }
2555
+ processedStrokes.push(spaint);
2556
+ } else {
2557
+ processedStrokes.push(stroke);
2443
2558
  }
2444
- return stroke;
2445
- });
2559
+ }
2446
2560
 
2447
2561
  node.strokes = processedStrokes;
2448
2562
 
@@ -2677,10 +2791,25 @@ figma.ui.onmessage = async (msg) => {
2677
2791
  throw new Error('Node must be a TEXT node. Got: ' + node.type);
2678
2792
  }
2679
2793
 
2680
- // Load the font first
2681
- await figma.loadFontAsync(node.fontName);
2682
-
2683
- node.characters = msg.text;
2794
+ // Load the font(s) first — mutating text in dynamic-page mode requires it.
2795
+ // If the caller requested a new family/style, load that (tolerating
2796
+ // "SemiBold" vs "Semi Bold"); otherwise load the node's existing font(s),
2797
+ // which also handles mixed-font nodes that the old single loadFontAsync
2798
+ // call would have crashed on.
2799
+ if (msg.fontFamily || msg.fontStyle) {
2800
+ var currentFont = (node.fontName && node.fontName !== figma.mixed)
2801
+ ? node.fontName
2802
+ : { family: 'Inter', style: 'Regular' };
2803
+ var targetFamily = msg.fontFamily || currentFont.family;
2804
+ var targetStyle = msg.fontStyle || currentFont.style;
2805
+ await loadFontsForNode(node); // existing runs, so setting characters is safe
2806
+ var loadedFont = await loadFontWithFallback(targetFamily, targetStyle);
2807
+ node.characters = msg.text;
2808
+ node.fontName = loadedFont;
2809
+ } else {
2810
+ await loadFontsForNode(node);
2811
+ node.characters = msg.text;
2812
+ }
2684
2813
 
2685
2814
  // Apply font properties if specified
2686
2815
  if (msg.fontSize) {
@@ -3049,7 +3178,7 @@ figma.ui.onmessage = async (msg) => {
3049
3178
  });
3050
3179
  // Short delay to let the response message be sent before reload
3051
3180
  setTimeout(function() {
3052
- figma.showUI(__html__, { width: 180, height: 50, visible: true, themeColors: true });
3181
+ figma.showUI(__html__, { width: 240, height: 40, visible: true, themeColors: true });
3053
3182
  }, 100);
3054
3183
  } catch (error) {
3055
3184
  var errorMsg = error && error.message ? error.message : String(error);
@@ -6131,8 +6260,8 @@ figma.ui.onmessage = async (msg) => {
6131
6260
  var textNode = figma.createText();
6132
6261
  var fontFamily = msg.fontFamily || 'Inter';
6133
6262
  var fontStyle = msg.fontStyle || 'Regular';
6134
- await figma.loadFontAsync({ family: fontFamily, style: fontStyle });
6135
- textNode.fontName = { family: fontFamily, style: fontStyle };
6263
+ var slideFont = await loadFontWithFallback(fontFamily, fontStyle);
6264
+ textNode.fontName = slideFont;
6136
6265
  textNode.characters = msg.text || '';
6137
6266
  textNode.fontSize = msg.fontSize || 24;
6138
6267
  textNode.x = typeof msg.x === 'number' ? msg.x : 100;