figma-console-mcp 1.41.0 → 1.41.2
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 +5 -3
- package/dist/core/deep-component-tools.d.ts.map +1 -1
- package/dist/core/deep-component-tools.js +9 -6
- package/dist/core/deep-component-tools.js.map +1 -1
- package/dist/core/design-system-manifest.d.ts +10 -0
- package/dist/core/design-system-manifest.d.ts.map +1 -1
- package/dist/core/design-system-manifest.js +30 -0
- package/dist/core/design-system-manifest.js.map +1 -1
- package/dist/core/figma-tools.d.ts.map +1 -1
- package/dist/core/figma-tools.js +30 -15
- package/dist/core/figma-tools.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 +3 -1
- package/dist/core/websocket-connector.js.map +1 -1
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +18 -15
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +146 -66
- package/figma-desktop-bridge/ui.html +2 -2
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// detect stale cached plugins. Bumped by scripts/release.sh ONLY when plugin files
|
|
10
10
|
// change (see issue #62); server-only releases leave it alone, so it may lag
|
|
11
11
|
// package.json — that's intentional, not drift.
|
|
12
|
-
var PLUGIN_VERSION = '1.
|
|
12
|
+
var PLUGIN_VERSION = '1.41.1'; // Last release in which plugin files changed.
|
|
13
13
|
|
|
14
14
|
console.log('🌉 [Desktop Bridge] Plugin loaded (v' + PLUGIN_VERSION + ')');
|
|
15
15
|
|
|
@@ -1196,6 +1196,16 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1196
1196
|
}
|
|
1197
1197
|
}
|
|
1198
1198
|
|
|
1199
|
+
// No stroked/frame child: the variant itself carries the visuals
|
|
1200
|
+
// (e.g. a button whose fill sits on the component).
|
|
1201
|
+
if (!mainChild) mainChild = variant;
|
|
1202
|
+
|
|
1203
|
+
// Geometry and structure, so variants that differ in size or content
|
|
1204
|
+
// (2 icons vs 6 icons) don't diff as identical (#121).
|
|
1205
|
+
sig.width = Math.round(variant.width * 100) / 100;
|
|
1206
|
+
sig.height = Math.round(variant.height * 100) / 100;
|
|
1207
|
+
sig.children = variant.children ? variant.children.map(function(c) { return c.name; }) : [];
|
|
1208
|
+
|
|
1199
1209
|
if (mainChild) {
|
|
1200
1210
|
var child = mainChild;
|
|
1201
1211
|
// This is the main interactive frame
|
|
@@ -1312,13 +1322,39 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1312
1322
|
var stateMachine = { states: {}, defaultState: null, cssMapping: {} };
|
|
1313
1323
|
var defaultSig = null;
|
|
1314
1324
|
|
|
1315
|
-
// Find the default variant (
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1325
|
+
// Find the default variant (#121). Start from Figma's own default (the
|
|
1326
|
+
// set's top-left variant). With a state axis, prefer the variant that
|
|
1327
|
+
// matches it on every other axis but is in a resting state. Variant
|
|
1328
|
+
// names are "Axis=Value, ..." in the author's casing, so compare values
|
|
1329
|
+
// case-insensitively.
|
|
1330
|
+
function parseAxes(name) {
|
|
1331
|
+
var out = {};
|
|
1332
|
+
var segs = name.split(',');
|
|
1333
|
+
for (var si = 0; si < segs.length; si++) {
|
|
1334
|
+
var kv = segs[si].split('=');
|
|
1335
|
+
if (kv.length === 2) out[kv[0].trim()] = kv[1].trim();
|
|
1336
|
+
}
|
|
1337
|
+
return out;
|
|
1338
|
+
}
|
|
1339
|
+
var figmaDefault = null;
|
|
1340
|
+
try { figmaDefault = node.defaultVariant || null; } catch (e) {}
|
|
1341
|
+
if (!figmaDefault && variants.length > 0) figmaDefault = variants[0];
|
|
1342
|
+
defaultVariant = figmaDefault;
|
|
1343
|
+
if (stateAxis && figmaDefault) {
|
|
1344
|
+
var restingStates = ['default', 'rest', 'resting', 'enabled', 'idle', 'normal', 'base'];
|
|
1345
|
+
var baseAxes = parseAxes(figmaDefault.name);
|
|
1346
|
+
var currentState = (baseAxes[stateAxis] || '').toLowerCase();
|
|
1347
|
+
if (restingStates.indexOf(currentState) === -1) {
|
|
1348
|
+
for (var di = 0; di < variants.length; di++) {
|
|
1349
|
+
var cand = parseAxes(variants[di].name);
|
|
1350
|
+
var sameOtherAxes = true;
|
|
1351
|
+
for (var bk in baseAxes) {
|
|
1352
|
+
if (bk !== stateAxis && cand[bk] !== baseAxes[bk]) { sameOtherAxes = false; break; }
|
|
1353
|
+
}
|
|
1354
|
+
if (sameOtherAxes && restingStates.indexOf((cand[stateAxis] || '').toLowerCase()) !== -1) {
|
|
1355
|
+
defaultVariant = variants[di];
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1322
1358
|
}
|
|
1323
1359
|
}
|
|
1324
1360
|
}
|
|
@@ -1352,6 +1388,18 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1352
1388
|
if (sig.textToken !== defaultSig.textToken) diff.textToken = sig.textToken || sig.textHex;
|
|
1353
1389
|
if (sig.opacity !== defaultSig.opacity) diff.opacity = sig.opacity;
|
|
1354
1390
|
if (JSON.stringify(sig.effects) !== JSON.stringify(defaultSig.effects)) diff.effects = sig.effects;
|
|
1391
|
+
if (sig.width !== defaultSig.width) diff.width = sig.width;
|
|
1392
|
+
if (sig.height !== defaultSig.height) diff.height = sig.height;
|
|
1393
|
+
if (sig.fillHex !== defaultSig.fillHex && !diff.fillToken) diff.fillHex = sig.fillHex;
|
|
1394
|
+
if (sig.strokeHex !== defaultSig.strokeHex && !diff.strokeToken) diff.strokeHex = sig.strokeHex;
|
|
1395
|
+
if (sig.textHex !== defaultSig.textHex && !diff.textToken) diff.textHex = sig.textHex;
|
|
1396
|
+
if (JSON.stringify(sig.children) !== JSON.stringify(defaultSig.children)) {
|
|
1397
|
+
diff.children = {
|
|
1398
|
+
count: sig.children.length,
|
|
1399
|
+
added: sig.children.filter(function(n) { return defaultSig.children.indexOf(n) === -1; }),
|
|
1400
|
+
removed: defaultSig.children.filter(function(n) { return sig.children.indexOf(n) === -1; })
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1355
1403
|
// Visibility changes not in default
|
|
1356
1404
|
var dvKeys = Object.keys(defaultSig.visibilityChanges);
|
|
1357
1405
|
var svKeys = Object.keys(sig.visibilityChanges);
|
|
@@ -1384,7 +1432,9 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1384
1432
|
}
|
|
1385
1433
|
|
|
1386
1434
|
if (defaultVariant) {
|
|
1387
|
-
|
|
1435
|
+
var defaultAxes = parseAxes(defaultVariant.name);
|
|
1436
|
+
stateMachine.defaultState = stateAxis ? (defaultAxes[stateAxis] || 'default') : 'default';
|
|
1437
|
+
stateMachine.defaultVariant = { id: defaultVariant.id, name: defaultVariant.name };
|
|
1388
1438
|
stateMachine.defaultSignature = defaultSig;
|
|
1389
1439
|
}
|
|
1390
1440
|
|
|
@@ -1428,7 +1478,9 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1428
1478
|
// ============================================================================
|
|
1429
1479
|
else if (msg.type === 'DEEP_GET_COMPONENT') {
|
|
1430
1480
|
try {
|
|
1431
|
-
var maxDepth = msg.depth
|
|
1481
|
+
var maxDepth = msg.depth;
|
|
1482
|
+
if (maxDepth === undefined || maxDepth === null) maxDepth = 10;
|
|
1483
|
+
if (maxDepth <= 0) maxDepth = Infinity; // 0 or negative = unlimited depth
|
|
1432
1484
|
console.log('🌉 [Desktop Bridge] Deep component fetch: ' + msg.nodeId + ' (depth: ' + maxDepth + ')');
|
|
1433
1485
|
|
|
1434
1486
|
var rootNode = await figma.getNodeByIdAsync(msg.nodeId);
|
|
@@ -1523,11 +1575,36 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1523
1575
|
} catch (e) {}
|
|
1524
1576
|
if (n.opacity !== undefined && n.opacity < 1) props.opacity = n.opacity;
|
|
1525
1577
|
|
|
1578
|
+
// Mixed corner radius -> per-corner radii (full info instead of dropping)
|
|
1579
|
+
try {
|
|
1580
|
+
if (n.cornerRadius === figma.mixed) {
|
|
1581
|
+
props.cornerRadius = 'MIXED';
|
|
1582
|
+
props.cornerRadiuses = {
|
|
1583
|
+
topLeft: n.topLeftRadius,
|
|
1584
|
+
topRight: n.topRightRadius,
|
|
1585
|
+
bottomRight: n.bottomRightRadius,
|
|
1586
|
+
bottomLeft: n.bottomLeftRadius
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
} catch (e) {}
|
|
1590
|
+
// Mixed stroke weight -> per-side weights
|
|
1591
|
+
try {
|
|
1592
|
+
if (n.strokeWeight === figma.mixed) {
|
|
1593
|
+
props.strokeWeight = 'MIXED';
|
|
1594
|
+
props.strokeWeights = {
|
|
1595
|
+
top: n.strokeTopWeight,
|
|
1596
|
+
right: n.strokeRightWeight,
|
|
1597
|
+
bottom: n.strokeBottomWeight,
|
|
1598
|
+
left: n.strokeLeftWeight
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
} catch (e) {}
|
|
1602
|
+
|
|
1526
1603
|
// Typography
|
|
1527
1604
|
if (n.type === 'TEXT') {
|
|
1528
1605
|
try { props.characters = n.characters; } catch (e) {}
|
|
1529
1606
|
try { if (n.fontSize !== figma.mixed) props.fontSize = n.fontSize; } catch (e) {}
|
|
1530
|
-
try { if (n.fontName !== figma.mixed) props.fontFamily = n.fontName.family; props.fontStyle = n.fontName.style; } catch (e) {}
|
|
1607
|
+
try { if (n.fontName !== figma.mixed) { props.fontFamily = n.fontName.family; props.fontStyle = n.fontName.style; } } catch (e) {}
|
|
1531
1608
|
try { if (n.fontWeight !== figma.mixed) props.fontWeight = n.fontWeight; } catch (e) {}
|
|
1532
1609
|
try { if (n.lineHeight !== figma.mixed) props.lineHeight = n.lineHeight; } catch (e) {}
|
|
1533
1610
|
try { if (n.letterSpacing !== figma.mixed) props.letterSpacing = n.letterSpacing; } catch (e) {}
|
|
@@ -1537,6 +1614,31 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1537
1614
|
try { if (n.textTruncation && n.textTruncation !== 'DISABLED') props.textTruncation = n.textTruncation; } catch (e) {}
|
|
1538
1615
|
try { if (n.textCase && n.textCase !== 'ORIGINAL') props.textCase = n.textCase; } catch (e) {}
|
|
1539
1616
|
try { if (n.textDecoration && n.textDecoration !== 'NONE') props.textDecoration = n.textDecoration; } catch (e) {}
|
|
1617
|
+
|
|
1618
|
+
// Expand mixed per-character styles into full per-range segments (no info loss).
|
|
1619
|
+
try {
|
|
1620
|
+
var _typoFields = ['fontName','fontSize','fontWeight','textDecoration','textCase','lineHeight','letterSpacing','fills','textStyleId','fillStyleId'];
|
|
1621
|
+
var _anyMixed = false;
|
|
1622
|
+
for (var _ti = 0; _ti < _typoFields.length; _ti++) {
|
|
1623
|
+
try { if (n[_typoFields[_ti]] === figma.mixed) { _anyMixed = true; break; } } catch (e2) {}
|
|
1624
|
+
}
|
|
1625
|
+
if (_anyMixed && typeof n.getStyledTextSegments === 'function') {
|
|
1626
|
+
props.styledSegments = n.getStyledTextSegments(_typoFields).map(function (s) {
|
|
1627
|
+
var o = { characters: s.characters, start: s.start, end: s.end };
|
|
1628
|
+
try { if (s.fontName) { o.fontFamily = s.fontName.family; o.fontStyle = s.fontName.style; } } catch (e3) {}
|
|
1629
|
+
if (s.fontSize !== undefined) o.fontSize = s.fontSize;
|
|
1630
|
+
if (s.fontWeight !== undefined) o.fontWeight = s.fontWeight;
|
|
1631
|
+
if (s.lineHeight !== undefined) o.lineHeight = s.lineHeight;
|
|
1632
|
+
if (s.letterSpacing !== undefined) o.letterSpacing = s.letterSpacing;
|
|
1633
|
+
if (s.textDecoration !== undefined) o.textDecoration = s.textDecoration;
|
|
1634
|
+
if (s.textCase !== undefined) o.textCase = s.textCase;
|
|
1635
|
+
if (s.fills !== undefined) o.fills = s.fills;
|
|
1636
|
+
if (s.textStyleId) o.textStyleId = s.textStyleId;
|
|
1637
|
+
if (s.fillStyleId) o.fillStyleId = s.fillStyleId;
|
|
1638
|
+
return o;
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
} catch (e) {}
|
|
1540
1642
|
}
|
|
1541
1643
|
|
|
1542
1644
|
// Design tokens (resolved to names)
|
|
@@ -1662,16 +1764,22 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1662
1764
|
|
|
1663
1765
|
var result = walkNode(rootNode, 0);
|
|
1664
1766
|
result._variableMapSize = Object.keys(varNameMap).length;
|
|
1665
|
-
result._maxDepthUsed = maxDepth;
|
|
1767
|
+
result._maxDepthUsed = (maxDepth === Infinity) ? 'unlimited' : maxDepth;
|
|
1666
1768
|
|
|
1667
|
-
|
|
1769
|
+
// figma.mixed is a Symbol; it cannot cross figma.ui.postMessage
|
|
1770
|
+
// (structuredClone throws "Cannot unwrap symbol"). Serialize with a
|
|
1771
|
+
// replacer that turns any Symbol into the string "MIXED", then send the
|
|
1772
|
+
// parsed (symbol-free) clone instead of the raw node tree.
|
|
1773
|
+
var resultJson = JSON.stringify(result, function (key, val) {
|
|
1774
|
+
return typeof val === 'symbol' ? 'MIXED' : val;
|
|
1775
|
+
});
|
|
1668
1776
|
console.log('🌉 [Desktop Bridge] Deep component data: ' + Math.round(resultJson.length / 1024) + 'KB, vars resolved: ' + Object.keys(varNameMap).length);
|
|
1669
1777
|
|
|
1670
1778
|
figma.ui.postMessage({
|
|
1671
1779
|
type: 'DEEP_GET_COMPONENT_RESULT',
|
|
1672
1780
|
requestId: msg.requestId,
|
|
1673
1781
|
success: true,
|
|
1674
|
-
data:
|
|
1782
|
+
data: JSON.parse(resultJson)
|
|
1675
1783
|
});
|
|
1676
1784
|
|
|
1677
1785
|
} catch (error) {
|
|
@@ -1733,6 +1841,8 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1733
1841
|
|
|
1734
1842
|
// Helper to extract component set data with all variants
|
|
1735
1843
|
function extractComponentSetData(node) {
|
|
1844
|
+
// The getter recomputes on every access; read it once per set
|
|
1845
|
+
var setPropDefs = node.componentPropertyDefinitions;
|
|
1736
1846
|
var variantAxes = {};
|
|
1737
1847
|
var variants = [];
|
|
1738
1848
|
|
|
@@ -1795,8 +1905,8 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1795
1905
|
variantAxes: axes,
|
|
1796
1906
|
variants: variants,
|
|
1797
1907
|
defaultVariant: variants.length > 0 ? variants[0] : null,
|
|
1798
|
-
properties:
|
|
1799
|
-
var propDef =
|
|
1908
|
+
properties: setPropDefs ? Object.keys(setPropDefs).map(function(propName) {
|
|
1909
|
+
var propDef = setPropDefs[propName];
|
|
1800
1910
|
return {
|
|
1801
1911
|
name: propName,
|
|
1802
1912
|
type: propDef.type,
|
|
@@ -1807,60 +1917,30 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1807
1917
|
};
|
|
1808
1918
|
}
|
|
1809
1919
|
|
|
1810
|
-
// Recursively search for components
|
|
1811
|
-
function findComponents(node) {
|
|
1812
|
-
if (!node) return;
|
|
1813
|
-
|
|
1814
|
-
if (node.type === 'COMPONENT_SET') {
|
|
1815
|
-
componentSets.push(extractComponentSetData(node));
|
|
1816
|
-
} else if (node.type === 'COMPONENT') {
|
|
1817
|
-
// Only add standalone components (not variants inside component sets)
|
|
1818
|
-
if (!node.parent || node.parent.type !== 'COMPONENT_SET') {
|
|
1819
|
-
components.push(extractComponentData(node, false));
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
// Recurse into children — skip unresolvable slot sublayers
|
|
1824
|
-
if (node.children) {
|
|
1825
|
-
node.children.forEach(function(child) {
|
|
1826
|
-
try { findComponents(child); } catch (e) { /* slot sublayer — skip */ }
|
|
1827
|
-
});
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
1920
|
// Load all pages first (required before accessing children)
|
|
1832
1921
|
console.log('🌉 [Desktop Bridge] Loading all pages...');
|
|
1833
1922
|
await figma.loadAllPagesAsync();
|
|
1834
1923
|
|
|
1835
|
-
//
|
|
1836
|
-
//
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
var
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
// Log progress for large files
|
|
1856
|
-
if (totalPages > PAGE_BATCH_SIZE) {
|
|
1857
|
-
console.log('🌉 [Desktop Bridge] Processed pages ' + (pageIndex + 1) + '-' + batchEnd + ' of ' + totalPages + ' (found ' + components.length + ' components so far)');
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
// Yield to event loop between batches to prevent UI freeze and allow cancellation
|
|
1861
|
-
if (batchEnd < totalPages) {
|
|
1862
|
-
await new Promise(function(resolve) { setTimeout(resolve, 0); });
|
|
1863
|
-
}
|
|
1924
|
+
// Native search instead of a JS walk over every node (#120): the walk
|
|
1925
|
+
// crossed into the Plugin API for each of the file's nodes, including
|
|
1926
|
+
// every layer inside every instance (89k nodes: 1.7s vs 0.13s here, and
|
|
1927
|
+
// far worse on heavier files).
|
|
1928
|
+
var found = figma.root.findAllWithCriteria({ types: ['COMPONENT_SET', 'COMPONENT'] });
|
|
1929
|
+
console.log('🌉 [Desktop Bridge] Found ' + found.length + ' component nodes; extracting...');
|
|
1930
|
+
for (var fi = 0; fi < found.length; fi++) {
|
|
1931
|
+
var fnode = found[fi];
|
|
1932
|
+
try {
|
|
1933
|
+
if (fnode.type === 'COMPONENT_SET') {
|
|
1934
|
+
componentSets.push(extractComponentSetData(fnode));
|
|
1935
|
+
} else if (!fnode.parent || fnode.parent.type !== 'COMPONENT_SET') {
|
|
1936
|
+
// Standalone components only (variants are reported under their set)
|
|
1937
|
+
components.push(extractComponentData(fnode, false));
|
|
1938
|
+
}
|
|
1939
|
+
} catch (e) { /* unresolvable slot sublayer — skip */ }
|
|
1940
|
+
// No setTimeout yields here: plugin timers are throttled when Figma is
|
|
1941
|
+
// in the background (up to ~1/min under Chromium's intensive
|
|
1942
|
+
// throttling), so a handful of yields turned a ~1.5s extraction into
|
|
1943
|
+
// the 300s timeout reported in #120.
|
|
1864
1944
|
}
|
|
1865
1945
|
|
|
1866
1946
|
console.log('🌉 [Desktop Bridge] Found ' + components.length + ' components and ' + componentSets.length + ' component sets');
|
|
@@ -1003,8 +1003,8 @@
|
|
|
1003
1003
|
window.deepGetComponent = (nodeId, depth) => {
|
|
1004
1004
|
return window.sendPluginCommand('DEEP_GET_COMPONENT', {
|
|
1005
1005
|
nodeId: nodeId,
|
|
1006
|
-
depth: depth || 10
|
|
1007
|
-
}, 30000)
|
|
1006
|
+
depth: (depth === undefined || depth === null) ? 10 : depth
|
|
1007
|
+
}, (typeof depth === 'number' && depth <= 0) ? 120000 : 30000)
|
|
1008
1008
|
.catch(function(err) { return { success: false, error: err.message || String(err) }; });
|
|
1009
1009
|
};
|
|
1010
1010
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-console-mcp",
|
|
3
|
-
"version": "1.41.
|
|
3
|
+
"version": "1.41.2",
|
|
4
4
|
"description": "The most comprehensive MCP server for Figma — design tokens, variables, components, write tools, version history diff, accessibility audits, FigJam, Slides, and more. Local (WebSocket Desktop Bridge plugin) and Cloudflare Workers (paired + remote) modes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/local.js",
|