@atezer/figma-mcp-bridge 1.3.1 → 1.4.0
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/CHANGELOG.md +45 -0
- package/README.md +4 -3
- package/dist/core/plugin-bridge-server.d.ts +16 -1
- package/dist/core/plugin-bridge-server.d.ts.map +1 -1
- package/dist/core/plugin-bridge-server.js +63 -2
- package/dist/core/plugin-bridge-server.js.map +1 -1
- package/dist/core/response-guard.d.ts +37 -0
- package/dist/core/response-guard.d.ts.map +1 -0
- package/dist/core/response-guard.js +166 -0
- package/dist/core/response-guard.js.map +1 -0
- package/dist/local-plugin-only.d.ts.map +1 -1
- package/dist/local-plugin-only.js +247 -1
- package/dist/local-plugin-only.js.map +1 -1
- package/dist/local.js +1 -1
- package/f-mcp-plugin/code.js +43 -0
- package/f-mcp-plugin/ui.html +186 -0
- package/package.json +1 -1
- package/dist/cloudflare/core/figma-style-extractor.js +0 -311
- package/dist/core/figma-style-extractor.d.ts +0 -76
- package/dist/core/figma-style-extractor.d.ts.map +0 -1
- package/dist/core/figma-style-extractor.js +0 -312
- package/dist/core/figma-style-extractor.js.map +0 -1
package/f-mcp-plugin/ui.html
CHANGED
|
@@ -218,6 +218,31 @@
|
|
|
218
218
|
<input type="number" id="mcp-port" min="5454" max="5470" value="5454" aria-label="MCP bridge port (auto fallback 5454-5470)" title="Elle değiştirirseniz tek porta kilitlenir; Otomatik tara veya Advanced'ı kapatın." />
|
|
219
219
|
</div>
|
|
220
220
|
<button type="button" id="auto-port-reset" class="auto-port-reset" title="Tek porta kilitlenmeyi kaldırır; 5454–5470 yeniden taranır (Claude config portu ile eşleşmeli)." aria-label="5454 ile 5470 arası otomatik port taraması">Otomatik tara</button>
|
|
221
|
+
<div class="api-token-section" style="margin-top:8px;border-top:1px solid rgba(255,255,255,0.08);padding-top:8px;">
|
|
222
|
+
<div style="display:flex;align-items:center;gap:6px;">
|
|
223
|
+
<label for="figma-token" style="font-size:11px;color:rgba(255,255,255,0.5);white-space:nowrap;">API Token</label>
|
|
224
|
+
<input type="password" id="figma-token" placeholder="figd_..." style="flex:1;font-size:11px;padding:3px 6px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);border-radius:4px;color:#fff;outline:none;" aria-label="Figma REST API token" />
|
|
225
|
+
<select id="token-expiry" style="font-size:10px;padding:2px 3px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);border-radius:3px;color:rgba(255,255,255,0.6);outline:none;" title="Token süresi (Figma'da seçtiğiniz süre)">
|
|
226
|
+
<option value="90">90g</option>
|
|
227
|
+
<option value="30">30g</option>
|
|
228
|
+
<option value="7">7g</option>
|
|
229
|
+
<option value="1">1g</option>
|
|
230
|
+
</select>
|
|
231
|
+
<button type="button" id="clear-token" style="font-size:10px;padding:2px 6px;background:rgba(255,100,100,0.15);border:1px solid rgba(255,100,100,0.2);border-radius:3px;color:rgba(255,100,100,0.8);cursor:pointer;" title="Token'ı temizle">x</button>
|
|
232
|
+
</div>
|
|
233
|
+
<div id="token-status" style="font-size:10px;color:rgba(255,255,255,0.35);margin-top:3px;display:none;"></div>
|
|
234
|
+
<div id="token-expiry-info" style="font-size:9px;color:rgba(255,255,255,0.3);margin-top:2px;display:none;"></div>
|
|
235
|
+
<div id="rate-limit-bar" style="display:none;margin-top:4px;">
|
|
236
|
+
<div style="display:flex;justify-content:space-between;font-size:9px;color:rgba(255,255,255,0.4);">
|
|
237
|
+
<span>API Limit</span>
|
|
238
|
+
<span id="rate-limit-text">—</span>
|
|
239
|
+
</div>
|
|
240
|
+
<div style="height:3px;background:rgba(255,255,255,0.08);border-radius:2px;overflow:hidden;margin-top:2px;">
|
|
241
|
+
<div id="rate-limit-fill" style="height:100%;background:#4ecdc4;border-radius:2px;transition:width 0.3s;width:100%;"></div>
|
|
242
|
+
</div>
|
|
243
|
+
<div id="rate-limit-warning" style="display:none;font-size:9px;margin-top:2px;font-weight:600;"></div>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
221
246
|
</div>
|
|
222
247
|
</div>
|
|
223
248
|
|
|
@@ -597,6 +622,35 @@
|
|
|
597
622
|
console.log('[F-MCP ATezer Bridge] File identity: ' + (msg.fileName || '?') + ' (' + (msg.fileKey || '?') + ')');
|
|
598
623
|
break;
|
|
599
624
|
|
|
625
|
+
case 'RESTORE_TOKEN':
|
|
626
|
+
// Token restored from figma.clientStorage on plugin open
|
|
627
|
+
if (msg.token) {
|
|
628
|
+
var tokenEl = document.getElementById('figma-token');
|
|
629
|
+
if (tokenEl) tokenEl.value = msg.token;
|
|
630
|
+
var expiresAt = msg.expiresAt || 0;
|
|
631
|
+
__tokenExpiresAt = expiresAt;
|
|
632
|
+
var isExpired = expiresAt > 0 && expiresAt < Date.now();
|
|
633
|
+
if (isExpired) {
|
|
634
|
+
updateTokenUI(false, null, expiresAt);
|
|
635
|
+
// Auto-delete expired token
|
|
636
|
+
parent.postMessage({ pluginMessage: { type: 'DELETE_TOKEN' } }, '*');
|
|
637
|
+
if (tokenEl) tokenEl.value = '';
|
|
638
|
+
} else {
|
|
639
|
+
updateTokenUI(true, null, expiresAt);
|
|
640
|
+
window.__pendingRestoreToken = msg.token;
|
|
641
|
+
}
|
|
642
|
+
console.log('[F-MCP] Token restored from clientStorage' + (isExpired ? ' (EXPIRED)' : '') + (expiresAt ? ' expires: ' + new Date(expiresAt).toLocaleDateString() : ''));
|
|
643
|
+
}
|
|
644
|
+
break;
|
|
645
|
+
|
|
646
|
+
case 'TOKEN_SAVED':
|
|
647
|
+
console.log('[F-MCP] Token saved to clientStorage: ' + (msg.success ? 'OK' : 'FAIL'));
|
|
648
|
+
break;
|
|
649
|
+
|
|
650
|
+
case 'TOKEN_DELETED':
|
|
651
|
+
console.log('[F-MCP] Token deleted from clientStorage');
|
|
652
|
+
break;
|
|
653
|
+
|
|
600
654
|
case 'VARIABLES_DATA':
|
|
601
655
|
window.__figmaVariablesData = msg.data;
|
|
602
656
|
window.__figmaVariablesReady = true;
|
|
@@ -815,6 +869,7 @@
|
|
|
815
869
|
var MCP_BRIDGE_PORT_KEY = 'f-mcp-bridge-port';
|
|
816
870
|
var MCP_BRIDGE_HOST_KEY = 'f-mcp-bridge-host';
|
|
817
871
|
var MCP_ADVANCED_OPEN_KEY = 'f-mcp-bridge-advanced-open';
|
|
872
|
+
|
|
818
873
|
var mcpBridgeWs = null;
|
|
819
874
|
var mcpBridgeReconnectTimer = null;
|
|
820
875
|
var mcpReconnectDelay = 1000;
|
|
@@ -858,6 +913,122 @@
|
|
|
858
913
|
requestUiResize();
|
|
859
914
|
}
|
|
860
915
|
|
|
916
|
+
// ---- Token UI ----
|
|
917
|
+
function updateTokenUI(hasToken, rateLimit, expiresAt) {
|
|
918
|
+
var statusEl = document.getElementById('token-status');
|
|
919
|
+
var barEl = document.getElementById('rate-limit-bar');
|
|
920
|
+
var textEl = document.getElementById('rate-limit-text');
|
|
921
|
+
var fillEl = document.getElementById('rate-limit-fill');
|
|
922
|
+
var expiryEl = document.getElementById('token-expiry-info');
|
|
923
|
+
if (statusEl) {
|
|
924
|
+
statusEl.style.display = 'block';
|
|
925
|
+
statusEl.textContent = hasToken ? 'Token aktif' : 'Token yok';
|
|
926
|
+
statusEl.style.color = hasToken ? 'rgba(78,205,196,0.8)' : 'rgba(255,255,255,0.35)';
|
|
927
|
+
}
|
|
928
|
+
// Expiry countdown
|
|
929
|
+
if (expiryEl) {
|
|
930
|
+
if (hasToken && expiresAt && expiresAt > 0) {
|
|
931
|
+
var now = Date.now();
|
|
932
|
+
var daysLeft = Math.ceil((expiresAt - now) / (24 * 60 * 60 * 1000));
|
|
933
|
+
if (daysLeft <= 0) {
|
|
934
|
+
expiryEl.style.display = 'block';
|
|
935
|
+
expiryEl.textContent = 'Token suresi dolmus!';
|
|
936
|
+
expiryEl.style.color = 'rgba(249,65,68,0.9)';
|
|
937
|
+
if (statusEl) { statusEl.textContent = 'Token suresi dolmus'; statusEl.style.color = 'rgba(249,65,68,0.8)'; }
|
|
938
|
+
} else {
|
|
939
|
+
expiryEl.style.display = 'block';
|
|
940
|
+
var expDate = new Date(expiresAt);
|
|
941
|
+
expiryEl.textContent = daysLeft + ' gun kaldi (' + expDate.toLocaleDateString('tr-TR') + ')';
|
|
942
|
+
expiryEl.style.color = daysLeft <= 7 ? 'rgba(249,193,79,0.8)' : 'rgba(255,255,255,0.35)';
|
|
943
|
+
}
|
|
944
|
+
} else {
|
|
945
|
+
expiryEl.style.display = 'none';
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
// Rate limit bar
|
|
949
|
+
if (rateLimit && rateLimit.limit > 0 && barEl && textEl && fillEl) {
|
|
950
|
+
barEl.style.display = 'block';
|
|
951
|
+
var pct = Math.round((rateLimit.remaining / rateLimit.limit) * 100);
|
|
952
|
+
textEl.textContent = rateLimit.remaining + ' / ' + rateLimit.limit;
|
|
953
|
+
fillEl.style.width = pct + '%';
|
|
954
|
+
fillEl.style.background = pct > 30 ? '#4ecdc4' : pct > 10 ? '#f9c74f' : '#f94144';
|
|
955
|
+
// Warning text
|
|
956
|
+
var warnEl = document.getElementById('rate-limit-warning');
|
|
957
|
+
if (warnEl) {
|
|
958
|
+
if (rateLimit.remaining === 0) {
|
|
959
|
+
warnEl.style.display = 'block';
|
|
960
|
+
warnEl.textContent = 'API limiti doldu!';
|
|
961
|
+
warnEl.style.color = '#f94144';
|
|
962
|
+
} else if (pct <= 5) {
|
|
963
|
+
warnEl.style.display = 'block';
|
|
964
|
+
warnEl.textContent = 'API limiti kritik!';
|
|
965
|
+
warnEl.style.color = '#f94144';
|
|
966
|
+
fillEl.style.animation = 'pulse 1s infinite';
|
|
967
|
+
} else if (pct <= 20) {
|
|
968
|
+
warnEl.style.display = 'block';
|
|
969
|
+
warnEl.textContent = 'Dusuk API limiti';
|
|
970
|
+
warnEl.style.color = '#f9c74f';
|
|
971
|
+
fillEl.style.animation = 'none';
|
|
972
|
+
} else {
|
|
973
|
+
warnEl.style.display = 'none';
|
|
974
|
+
fillEl.style.animation = 'none';
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
} else if (barEl) {
|
|
978
|
+
barEl.style.display = hasToken ? 'block' : 'none';
|
|
979
|
+
if (textEl) textEl.textContent = '—';
|
|
980
|
+
if (fillEl) { fillEl.style.width = '100%'; fillEl.style.animation = 'none'; }
|
|
981
|
+
var warnEl2 = document.getElementById('rate-limit-warning');
|
|
982
|
+
if (warnEl2) warnEl2.style.display = 'none';
|
|
983
|
+
}
|
|
984
|
+
requestUiResize();
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Track current token expiry for UI updates
|
|
988
|
+
var __tokenExpiresAt = 0;
|
|
989
|
+
|
|
990
|
+
function getSelectedExpiryDays() {
|
|
991
|
+
var sel = document.getElementById('token-expiry');
|
|
992
|
+
return sel ? parseInt(sel.value, 10) || 90 : 90;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function initTokenInput() {
|
|
996
|
+
var el = document.getElementById('figma-token');
|
|
997
|
+
var clearBtn = document.getElementById('clear-token');
|
|
998
|
+
if (el) {
|
|
999
|
+
el.addEventListener('change', function() {
|
|
1000
|
+
var token = (el.value || '').trim();
|
|
1001
|
+
if (token) {
|
|
1002
|
+
var days = getSelectedExpiryDays();
|
|
1003
|
+
var expiresAt = Date.now() + days * 24 * 60 * 60 * 1000;
|
|
1004
|
+
__tokenExpiresAt = expiresAt;
|
|
1005
|
+
// Save to Figma clientStorage (persists across plugin close/reopen)
|
|
1006
|
+
parent.postMessage({ pluginMessage: { type: 'SAVE_TOKEN', token: token, expiresAt: expiresAt } }, '*');
|
|
1007
|
+
// Send to bridge
|
|
1008
|
+
if (mcpBridgeWs && mcpBridgeWs.readyState === 1) {
|
|
1009
|
+
mcpBridgeWs.send(JSON.stringify({ type: 'setToken', token: token }));
|
|
1010
|
+
}
|
|
1011
|
+
updateTokenUI(true, null, expiresAt);
|
|
1012
|
+
} else {
|
|
1013
|
+
updateTokenUI(false, null, 0);
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
if (clearBtn) {
|
|
1018
|
+
clearBtn.addEventListener('click', function() {
|
|
1019
|
+
if (el) el.value = '';
|
|
1020
|
+
__tokenExpiresAt = 0;
|
|
1021
|
+
// Delete from Figma clientStorage
|
|
1022
|
+
parent.postMessage({ pluginMessage: { type: 'DELETE_TOKEN' } }, '*');
|
|
1023
|
+
// Clear from bridge
|
|
1024
|
+
if (mcpBridgeWs && mcpBridgeWs.readyState === 1) {
|
|
1025
|
+
mcpBridgeWs.send(JSON.stringify({ type: 'clearToken' }));
|
|
1026
|
+
}
|
|
1027
|
+
updateTokenUI(false, null, 0);
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
861
1032
|
function initAdvancedPanel() {
|
|
862
1033
|
var toggle = document.getElementById('advanced-toggle');
|
|
863
1034
|
var initial = false;
|
|
@@ -1057,6 +1228,20 @@
|
|
|
1057
1228
|
}
|
|
1058
1229
|
console.log('[F-MCP ATezer Bridge] Handshake OK — bridge v' + (msg.bridgeVersion || '?') + ' on port ' + mcpConnectedPort);
|
|
1059
1230
|
updateStatus('ready (:' + mcpConnectedPort + ')', true, false);
|
|
1231
|
+
// Resend restored token to bridge after handshake
|
|
1232
|
+
if (window.__pendingRestoreToken && currentWs && currentWs.readyState === 1) {
|
|
1233
|
+
currentWs.send(JSON.stringify({ type: 'setToken', token: window.__pendingRestoreToken }));
|
|
1234
|
+
console.log('[F-MCP] Sent restored token to bridge');
|
|
1235
|
+
}
|
|
1236
|
+
// Also check input field (user may have entered token before connection)
|
|
1237
|
+
var tokenInput = document.getElementById('figma-token');
|
|
1238
|
+
if (!window.__pendingRestoreToken && tokenInput && tokenInput.value && currentWs && currentWs.readyState === 1) {
|
|
1239
|
+
currentWs.send(JSON.stringify({ type: 'setToken', token: tokenInput.value.trim() }));
|
|
1240
|
+
}
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (msg.type === 'tokenStatus') {
|
|
1244
|
+
updateTokenUI(msg.hasToken, msg.rateLimit || null, __tokenExpiresAt);
|
|
1060
1245
|
return;
|
|
1061
1246
|
}
|
|
1062
1247
|
if (msg.type === 'ping') {
|
|
@@ -1268,6 +1453,7 @@
|
|
|
1268
1453
|
if (typeof WebSocket !== 'undefined') {
|
|
1269
1454
|
initMcpHostInput();
|
|
1270
1455
|
initMcpPortInput();
|
|
1456
|
+
initTokenInput();
|
|
1271
1457
|
initAdvancedPanel();
|
|
1272
1458
|
try {
|
|
1273
1459
|
var rememberedPort = parseInt(localStorage.getItem(MCP_BRIDGE_PORT_KEY) || '', 10);
|
package/package.json
CHANGED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Figma Style Extractor
|
|
3
|
-
*
|
|
4
|
-
* Extracts style information (colors, typography, spacing) from Figma files
|
|
5
|
-
* using the REST API /files endpoint. This provides an alternative to the
|
|
6
|
-
* Enterprise-only Variables API by parsing style data directly from nodes.
|
|
7
|
-
*
|
|
8
|
-
* Based on the approach used by Figma-Context-MCP
|
|
9
|
-
*/
|
|
10
|
-
import { logger } from './logger';
|
|
11
|
-
export class FigmaStyleExtractor {
|
|
12
|
-
constructor() {
|
|
13
|
-
this.extractedVariables = new Map();
|
|
14
|
-
this.colorIndex = 0;
|
|
15
|
-
this.typographyIndex = 0;
|
|
16
|
-
this.spacingIndex = 0;
|
|
17
|
-
this.radiusIndex = 0;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Extract style "variables" from Figma file data
|
|
21
|
-
* This mimics what users would see as variables in Figma
|
|
22
|
-
*/
|
|
23
|
-
async extractStylesFromFile(fileData) {
|
|
24
|
-
try {
|
|
25
|
-
logger.info('Extracting styles from Figma file data');
|
|
26
|
-
this.extractedVariables.clear();
|
|
27
|
-
this.colorIndex = 0;
|
|
28
|
-
this.typographyIndex = 0;
|
|
29
|
-
this.spacingIndex = 0;
|
|
30
|
-
this.radiusIndex = 0;
|
|
31
|
-
// Process the document tree
|
|
32
|
-
if (fileData.document) {
|
|
33
|
-
this.processNode(fileData.document);
|
|
34
|
-
}
|
|
35
|
-
// Also process components for more style data
|
|
36
|
-
if (fileData.components) {
|
|
37
|
-
Object.values(fileData.components).forEach((component) => {
|
|
38
|
-
if (component.node) {
|
|
39
|
-
this.processNode(component.node);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
// Process styles if available
|
|
44
|
-
if (fileData.styles) {
|
|
45
|
-
this.processStyles(fileData.styles);
|
|
46
|
-
}
|
|
47
|
-
const variables = Array.from(this.extractedVariables.values());
|
|
48
|
-
logger.info({
|
|
49
|
-
colorCount: variables.filter(v => v.type === 'COLOR').length,
|
|
50
|
-
typographyCount: variables.filter(v => v.type === 'TYPOGRAPHY').length,
|
|
51
|
-
spacingCount: variables.filter(v => v.type === 'SPACING').length,
|
|
52
|
-
radiusCount: variables.filter(v => v.type === 'RADIUS').length,
|
|
53
|
-
totalCount: variables.length
|
|
54
|
-
}, 'Extracted style variables from file');
|
|
55
|
-
return variables;
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
logger.error({ error }, 'Failed to extract styles from file');
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Process a single node and extract style information
|
|
64
|
-
*/
|
|
65
|
-
processNode(node, depth = 0) {
|
|
66
|
-
if (!node || depth > 10)
|
|
67
|
-
return; // Limit depth to prevent infinite recursion
|
|
68
|
-
// Extract colors from fills
|
|
69
|
-
if (node.fills && Array.isArray(node.fills)) {
|
|
70
|
-
node.fills.forEach(fill => {
|
|
71
|
-
if (fill.type === 'SOLID' && fill.color && fill.visible !== false) {
|
|
72
|
-
this.extractColor(fill.color, fill.opacity, node);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
// Extract colors from strokes
|
|
77
|
-
if (node.strokes && Array.isArray(node.strokes)) {
|
|
78
|
-
node.strokes.forEach(stroke => {
|
|
79
|
-
if (stroke.type === 'SOLID' && stroke.color && stroke.visible !== false) {
|
|
80
|
-
this.extractColor(stroke.color, stroke.opacity, node, 'stroke');
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
// Extract typography styles
|
|
85
|
-
if (node.type === 'TEXT' && node.style) {
|
|
86
|
-
this.extractTypography(node.style, node);
|
|
87
|
-
}
|
|
88
|
-
// Extract spacing from auto-layout
|
|
89
|
-
if (node.layoutMode) {
|
|
90
|
-
if (node.itemSpacing !== undefined && node.itemSpacing > 0) {
|
|
91
|
-
this.extractSpacing('spacing', node.itemSpacing, node);
|
|
92
|
-
}
|
|
93
|
-
if (node.paddingLeft !== undefined && node.paddingLeft > 0) {
|
|
94
|
-
this.extractSpacing('padding', node.paddingLeft, node);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
// Extract corner radius
|
|
98
|
-
if (node.cornerRadius !== undefined && node.cornerRadius > 0) {
|
|
99
|
-
this.extractRadius(node.cornerRadius, node);
|
|
100
|
-
}
|
|
101
|
-
else if (node.rectangleCornerRadii && node.rectangleCornerRadii.length > 0) {
|
|
102
|
-
const uniqueRadii = [...new Set(node.rectangleCornerRadii)];
|
|
103
|
-
uniqueRadii.forEach(radius => {
|
|
104
|
-
if (radius > 0) {
|
|
105
|
-
this.extractRadius(radius, node);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
// Process children recursively
|
|
110
|
-
if (node.children && Array.isArray(node.children)) {
|
|
111
|
-
node.children.forEach(child => {
|
|
112
|
-
this.processNode(child, depth + 1);
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Extract color variable
|
|
118
|
-
*/
|
|
119
|
-
extractColor(color, opacity = 1, node, type = 'fill') {
|
|
120
|
-
const r = Math.round(color.r * 255);
|
|
121
|
-
const g = Math.round(color.g * 255);
|
|
122
|
-
const b = Math.round(color.b * 255);
|
|
123
|
-
const a = opacity * (color.a || 1);
|
|
124
|
-
const hex = '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
|
|
125
|
-
const rgba = a < 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : hex;
|
|
126
|
-
// Create a unique key based on the color value
|
|
127
|
-
const key = `color_${hex}_${a}`;
|
|
128
|
-
if (!this.extractedVariables.has(key)) {
|
|
129
|
-
// Generate a meaningful name based on the node
|
|
130
|
-
const category = this.inferColorCategory(node.name);
|
|
131
|
-
const name = this.generateColorName(category, type, this.colorIndex++);
|
|
132
|
-
this.extractedVariables.set(key, {
|
|
133
|
-
id: key,
|
|
134
|
-
name,
|
|
135
|
-
value: rgba,
|
|
136
|
-
type: 'COLOR',
|
|
137
|
-
category,
|
|
138
|
-
description: `Extracted from ${node.name || 'unnamed node'}`,
|
|
139
|
-
nodeId: node.id
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Extract typography variable
|
|
145
|
-
*/
|
|
146
|
-
extractTypography(style, node) {
|
|
147
|
-
const key = `typography_${style.fontFamily}_${style.fontSize}_${style.fontWeight}`;
|
|
148
|
-
if (!this.extractedVariables.has(key)) {
|
|
149
|
-
const name = this.generateTypographyName(node.name, this.typographyIndex++);
|
|
150
|
-
const value = [
|
|
151
|
-
`font-family: "${style.fontFamily || 'Inter'}"`,
|
|
152
|
-
style.fontSize ? `font-size: ${style.fontSize}px` : '',
|
|
153
|
-
style.fontWeight ? `font-weight: ${style.fontWeight}` : '',
|
|
154
|
-
style.lineHeightPx ? `line-height: ${style.lineHeightPx}px` : '',
|
|
155
|
-
style.letterSpacing ? `letter-spacing: ${style.letterSpacing}px` : ''
|
|
156
|
-
].filter(Boolean).join(', ');
|
|
157
|
-
this.extractedVariables.set(key, {
|
|
158
|
-
id: key,
|
|
159
|
-
name,
|
|
160
|
-
value,
|
|
161
|
-
type: 'TYPOGRAPHY',
|
|
162
|
-
category: 'text',
|
|
163
|
-
description: `Extracted from ${node.name || 'unnamed text'}`,
|
|
164
|
-
nodeId: node.id
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Extract spacing variable
|
|
170
|
-
*/
|
|
171
|
-
extractSpacing(type, value, node) {
|
|
172
|
-
const key = `spacing_${type}_${value}`;
|
|
173
|
-
if (!this.extractedVariables.has(key)) {
|
|
174
|
-
const name = `${type}/${Math.round(value / 4) * 4 || value}`; // Round to nearest 4px
|
|
175
|
-
this.extractedVariables.set(key, {
|
|
176
|
-
id: key,
|
|
177
|
-
name,
|
|
178
|
-
value: `${value}px`,
|
|
179
|
-
type: 'SPACING',
|
|
180
|
-
category: type,
|
|
181
|
-
description: `Extracted from ${node.name || 'unnamed node'}`,
|
|
182
|
-
nodeId: node.id
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Extract radius variable
|
|
188
|
-
*/
|
|
189
|
-
extractRadius(value, node) {
|
|
190
|
-
const key = `radius_${value}`;
|
|
191
|
-
if (!this.extractedVariables.has(key)) {
|
|
192
|
-
const name = `radius/${this.categorizeRadius(value)}`;
|
|
193
|
-
this.extractedVariables.set(key, {
|
|
194
|
-
id: key,
|
|
195
|
-
name,
|
|
196
|
-
value: `${value}px`,
|
|
197
|
-
type: 'RADIUS',
|
|
198
|
-
category: 'border',
|
|
199
|
-
description: `Extracted from ${node.name || 'unnamed node'}`,
|
|
200
|
-
nodeId: node.id
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Process Figma styles object
|
|
206
|
-
*/
|
|
207
|
-
processStyles(styles) {
|
|
208
|
-
Object.entries(styles).forEach(([styleId, styleData]) => {
|
|
209
|
-
const { name, description, styleType } = styleData;
|
|
210
|
-
if (styleType === 'FILL' || styleType === 'TEXT') {
|
|
211
|
-
// These are named styles that could be considered variables
|
|
212
|
-
const variable = {
|
|
213
|
-
id: styleId,
|
|
214
|
-
name: name || styleId,
|
|
215
|
-
value: styleId, // We don't have the actual value here
|
|
216
|
-
type: styleType === 'FILL' ? 'COLOR' : 'TYPOGRAPHY',
|
|
217
|
-
description: description || `Style: ${name}`,
|
|
218
|
-
category: 'style'
|
|
219
|
-
};
|
|
220
|
-
this.extractedVariables.set(`style_${styleId}`, variable);
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Helper to infer color category from node name
|
|
226
|
-
*/
|
|
227
|
-
inferColorCategory(nodeName) {
|
|
228
|
-
if (!nodeName)
|
|
229
|
-
return 'color';
|
|
230
|
-
const name = nodeName.toLowerCase();
|
|
231
|
-
if (name.includes('background') || name.includes('bg'))
|
|
232
|
-
return 'background';
|
|
233
|
-
if (name.includes('text') || name.includes('label') || name.includes('title'))
|
|
234
|
-
return 'text';
|
|
235
|
-
if (name.includes('border') || name.includes('stroke'))
|
|
236
|
-
return 'border';
|
|
237
|
-
if (name.includes('primary') || name.includes('secondary') || name.includes('accent'))
|
|
238
|
-
return 'theme';
|
|
239
|
-
if (name.includes('success') || name.includes('error') || name.includes('warning'))
|
|
240
|
-
return 'semantic';
|
|
241
|
-
return 'color';
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Generate a meaningful color name
|
|
245
|
-
*/
|
|
246
|
-
generateColorName(category, type, index) {
|
|
247
|
-
const tier = index < 5 ? 'primary' : index < 10 ? 'secondary' : 'tertiary';
|
|
248
|
-
return `${category}/${tier}-${type}`;
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Generate a meaningful typography name
|
|
252
|
-
*/
|
|
253
|
-
generateTypographyName(nodeName, index) {
|
|
254
|
-
if (nodeName) {
|
|
255
|
-
const name = nodeName.toLowerCase();
|
|
256
|
-
if (name.includes('heading') || name.includes('h1') || name.includes('h2')) {
|
|
257
|
-
return `heading/${name.replace(/[^a-z0-9]/g, '-')}`;
|
|
258
|
-
}
|
|
259
|
-
if (name.includes('body') || name.includes('paragraph')) {
|
|
260
|
-
return `body/${name.replace(/[^a-z0-9]/g, '-')}`;
|
|
261
|
-
}
|
|
262
|
-
if (name.includes('caption') || name.includes('label')) {
|
|
263
|
-
return `caption/${name.replace(/[^a-z0-9]/g, '-')}`;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return `text/style-${index}`;
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* Categorize radius values
|
|
270
|
-
*/
|
|
271
|
-
categorizeRadius(value) {
|
|
272
|
-
if (value === 0)
|
|
273
|
-
return 'none';
|
|
274
|
-
if (value <= 2)
|
|
275
|
-
return 'xs';
|
|
276
|
-
if (value <= 4)
|
|
277
|
-
return 'sm';
|
|
278
|
-
if (value <= 8)
|
|
279
|
-
return 'md';
|
|
280
|
-
if (value <= 16)
|
|
281
|
-
return 'lg';
|
|
282
|
-
if (value <= 24)
|
|
283
|
-
return 'xl';
|
|
284
|
-
return 'xxl';
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Format the extracted variables for output
|
|
288
|
-
*/
|
|
289
|
-
formatVariablesAsOutput(variables) {
|
|
290
|
-
// Group variables by type and category
|
|
291
|
-
const grouped = {};
|
|
292
|
-
variables.forEach(variable => {
|
|
293
|
-
const key = variable.name;
|
|
294
|
-
grouped[key] = variable.value;
|
|
295
|
-
});
|
|
296
|
-
// Add metadata about extraction method
|
|
297
|
-
grouped['_metadata'] = {
|
|
298
|
-
extractionMethod: 'REST_API_STYLES',
|
|
299
|
-
note: 'These are extracted style properties, not true Figma Variables (which require Enterprise)',
|
|
300
|
-
timestamp: new Date().toISOString(),
|
|
301
|
-
counts: {
|
|
302
|
-
colors: variables.filter(v => v.type === 'COLOR').length,
|
|
303
|
-
typography: variables.filter(v => v.type === 'TYPOGRAPHY').length,
|
|
304
|
-
spacing: variables.filter(v => v.type === 'SPACING').length,
|
|
305
|
-
radius: variables.filter(v => v.type === 'RADIUS').length,
|
|
306
|
-
total: variables.length
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
return grouped;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Figma Style Extractor
|
|
3
|
-
*
|
|
4
|
-
* Extracts style information (colors, typography, spacing) from Figma files
|
|
5
|
-
* using the REST API /files endpoint. This provides an alternative to the
|
|
6
|
-
* Enterprise-only Variables API by parsing style data directly from nodes.
|
|
7
|
-
*
|
|
8
|
-
* Based on the approach used by Figma-Context-MCP
|
|
9
|
-
*/
|
|
10
|
-
interface ExtractedVariable {
|
|
11
|
-
id: string;
|
|
12
|
-
name: string;
|
|
13
|
-
value: string;
|
|
14
|
-
type: 'COLOR' | 'TYPOGRAPHY' | 'SPACING' | 'RADIUS' | 'EFFECT';
|
|
15
|
-
category?: string;
|
|
16
|
-
description?: string;
|
|
17
|
-
nodeId?: string;
|
|
18
|
-
}
|
|
19
|
-
export declare class FigmaStyleExtractor {
|
|
20
|
-
private extractedVariables;
|
|
21
|
-
private colorIndex;
|
|
22
|
-
private typographyIndex;
|
|
23
|
-
private spacingIndex;
|
|
24
|
-
private radiusIndex;
|
|
25
|
-
/**
|
|
26
|
-
* Extract style "variables" from Figma file data
|
|
27
|
-
* This mimics what users would see as variables in Figma
|
|
28
|
-
*/
|
|
29
|
-
extractStylesFromFile(fileData: any): Promise<ExtractedVariable[]>;
|
|
30
|
-
/**
|
|
31
|
-
* Process a single node and extract style information
|
|
32
|
-
*/
|
|
33
|
-
private processNode;
|
|
34
|
-
/**
|
|
35
|
-
* Extract color variable
|
|
36
|
-
*/
|
|
37
|
-
private extractColor;
|
|
38
|
-
/**
|
|
39
|
-
* Extract typography variable
|
|
40
|
-
*/
|
|
41
|
-
private extractTypography;
|
|
42
|
-
/**
|
|
43
|
-
* Extract spacing variable
|
|
44
|
-
*/
|
|
45
|
-
private extractSpacing;
|
|
46
|
-
/**
|
|
47
|
-
* Extract radius variable
|
|
48
|
-
*/
|
|
49
|
-
private extractRadius;
|
|
50
|
-
/**
|
|
51
|
-
* Process Figma styles object
|
|
52
|
-
*/
|
|
53
|
-
private processStyles;
|
|
54
|
-
/**
|
|
55
|
-
* Helper to infer color category from node name
|
|
56
|
-
*/
|
|
57
|
-
private inferColorCategory;
|
|
58
|
-
/**
|
|
59
|
-
* Generate a meaningful color name
|
|
60
|
-
*/
|
|
61
|
-
private generateColorName;
|
|
62
|
-
/**
|
|
63
|
-
* Generate a meaningful typography name
|
|
64
|
-
*/
|
|
65
|
-
private generateTypographyName;
|
|
66
|
-
/**
|
|
67
|
-
* Categorize radius values
|
|
68
|
-
*/
|
|
69
|
-
private categorizeRadius;
|
|
70
|
-
/**
|
|
71
|
-
* Format the extracted variables for output
|
|
72
|
-
*/
|
|
73
|
-
formatVariablesAsOutput(variables: ExtractedVariable[]): any;
|
|
74
|
-
}
|
|
75
|
-
export {};
|
|
76
|
-
//# sourceMappingURL=figma-style-extractor.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"figma-style-extractor.d.ts","sourceRoot":"","sources":["../../src/core/figma-style-extractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqDH,UAAU,iBAAiB;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,kBAAkB,CAA6C;IACvE,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,WAAW,CAAK;IAExB;;;OAGG;IACG,qBAAqB,CAAC,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAiDxE;;OAEG;IACH,OAAO,CAAC,WAAW;IAwDnB;;OAEG;IACH,OAAO,CAAC,YAAY;IA6BpB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA0BzB;;OAEG;IACH,OAAO,CAAC,cAAc;IAkBtB;;OAEG;IACH,OAAO,CAAC,aAAa;IAkBrB;;OAEG;IACH,OAAO,CAAC,aAAa;IAoBrB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAc1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAKzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAiB9B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAUxB;;OAEG;IACH,uBAAuB,CAAC,SAAS,EAAE,iBAAiB,EAAE,GAAG,GAAG;CAyB7D"}
|