@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.
- package/README.md +4 -2
- package/dist/cloudflare/core/cloud-websocket-connector.js +2 -0
- package/dist/cloudflare/core/port-discovery.js +130 -9
- package/dist/cloudflare/core/tokens-tools.js +1 -1
- package/dist/cloudflare/core/websocket-connector.js +2 -0
- package/dist/cloudflare/core/write-tools.js +35 -8
- package/dist/cloudflare/index.js +3 -3
- package/dist/core/port-discovery.d.ts +30 -1
- package/dist/core/port-discovery.d.ts.map +1 -1
- package/dist/core/port-discovery.js +130 -9
- package/dist/core/port-discovery.js.map +1 -1
- package/dist/core/tokens-tools.js +1 -1
- package/dist/core/websocket-connector.d.ts.map +1 -1
- package/dist/core/websocket-connector.js +2 -0
- package/dist/core/websocket-connector.js.map +1 -1
- package/dist/core/write-tools.d.ts.map +1 -1
- package/dist/core/write-tools.js +35 -8
- package/dist/core/write-tools.js.map +1 -1
- package/dist/local.d.ts +2 -0
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +40 -10
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/README.md +54 -2
- package/figma-desktop-bridge/code.js +158 -29
- package/figma-desktop-bridge/ui.html +985 -111
- package/package.json +5 -3
|
@@ -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.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
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
|
-
|
|
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
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
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:
|
|
2441
|
-
opacity:
|
|
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
|
-
|
|
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
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
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:
|
|
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
|
|
6135
|
-
textNode.fontName =
|
|
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;
|