@atezer/figma-mcp-bridge 1.1.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/LICENSE +21 -0
- package/README.md +241 -0
- package/dist/browser/base.d.ts +50 -0
- package/dist/browser/base.d.ts.map +1 -0
- package/dist/browser/base.js +6 -0
- package/dist/browser/base.js.map +1 -0
- package/dist/browser/local.d.ts +81 -0
- package/dist/browser/local.d.ts.map +1 -0
- package/dist/browser/local.js +283 -0
- package/dist/browser/local.js.map +1 -0
- package/dist/cloudflare/browser/base.js +5 -0
- package/dist/cloudflare/browser/cloudflare.js +156 -0
- package/dist/cloudflare/browser-manager.js +157 -0
- package/dist/cloudflare/core/audit-log.js +62 -0
- package/dist/cloudflare/core/config.js +163 -0
- package/dist/cloudflare/core/console-monitor.js +427 -0
- package/dist/cloudflare/core/design-system-manifest.js +260 -0
- package/dist/cloudflare/core/enrichment/enrichment-service.js +272 -0
- package/dist/cloudflare/core/enrichment/index.js +7 -0
- package/dist/cloudflare/core/enrichment/relationship-mapper.js +351 -0
- package/dist/cloudflare/core/enrichment/style-resolver.js +326 -0
- package/dist/cloudflare/core/figma-api.js +273 -0
- package/dist/cloudflare/core/figma-desktop-connector.js +1029 -0
- package/dist/cloudflare/core/figma-reconstruction-spec.js +402 -0
- package/dist/cloudflare/core/figma-style-extractor.js +311 -0
- package/dist/cloudflare/core/figma-tools.js +2883 -0
- package/dist/cloudflare/core/logger.js +53 -0
- package/dist/cloudflare/core/plugin-bridge-connector.js +154 -0
- package/dist/cloudflare/core/plugin-bridge-server.js +174 -0
- package/dist/cloudflare/core/snippet-injector.js +96 -0
- package/dist/cloudflare/core/types/enriched.js +5 -0
- package/dist/cloudflare/core/types/index.js +4 -0
- package/dist/cloudflare/index.js +1061 -0
- package/dist/cloudflare/test-browser.js +88 -0
- package/dist/core/audit-log.d.ts +26 -0
- package/dist/core/audit-log.d.ts.map +1 -0
- package/dist/core/audit-log.js +63 -0
- package/dist/core/audit-log.js.map +1 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +164 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/console-monitor.d.ts +82 -0
- package/dist/core/console-monitor.d.ts.map +1 -0
- package/dist/core/console-monitor.js +428 -0
- package/dist/core/console-monitor.js.map +1 -0
- package/dist/core/design-system-manifest.d.ts +272 -0
- package/dist/core/design-system-manifest.d.ts.map +1 -0
- package/dist/core/design-system-manifest.js +261 -0
- package/dist/core/design-system-manifest.js.map +1 -0
- package/dist/core/enrichment/enrichment-service.d.ts +52 -0
- package/dist/core/enrichment/enrichment-service.d.ts.map +1 -0
- package/dist/core/enrichment/enrichment-service.js +273 -0
- package/dist/core/enrichment/enrichment-service.js.map +1 -0
- package/dist/core/enrichment/index.d.ts +8 -0
- package/dist/core/enrichment/index.d.ts.map +1 -0
- package/dist/core/enrichment/index.js +8 -0
- package/dist/core/enrichment/index.js.map +1 -0
- package/dist/core/enrichment/relationship-mapper.d.ts +106 -0
- package/dist/core/enrichment/relationship-mapper.d.ts.map +1 -0
- package/dist/core/enrichment/relationship-mapper.js +352 -0
- package/dist/core/enrichment/relationship-mapper.js.map +1 -0
- package/dist/core/enrichment/style-resolver.d.ts +80 -0
- package/dist/core/enrichment/style-resolver.d.ts.map +1 -0
- package/dist/core/enrichment/style-resolver.js +327 -0
- package/dist/core/enrichment/style-resolver.js.map +1 -0
- package/dist/core/figma-api.d.ts +137 -0
- package/dist/core/figma-api.d.ts.map +1 -0
- package/dist/core/figma-api.js +274 -0
- package/dist/core/figma-api.js.map +1 -0
- package/dist/core/figma-desktop-connector.d.ts +238 -0
- package/dist/core/figma-desktop-connector.d.ts.map +1 -0
- package/dist/core/figma-desktop-connector.js +1030 -0
- package/dist/core/figma-desktop-connector.js.map +1 -0
- package/dist/core/figma-reconstruction-spec.d.ts +166 -0
- package/dist/core/figma-reconstruction-spec.d.ts.map +1 -0
- package/dist/core/figma-reconstruction-spec.js +403 -0
- package/dist/core/figma-reconstruction-spec.js.map +1 -0
- package/dist/core/figma-style-extractor.d.ts +76 -0
- package/dist/core/figma-style-extractor.d.ts.map +1 -0
- package/dist/core/figma-style-extractor.js +312 -0
- package/dist/core/figma-style-extractor.js.map +1 -0
- package/dist/core/figma-tools.d.ts +21 -0
- package/dist/core/figma-tools.d.ts.map +1 -0
- package/dist/core/figma-tools.js +2884 -0
- package/dist/core/figma-tools.js.map +1 -0
- package/dist/core/logger.d.ts +22 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +54 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/plugin-bridge-connector.d.ts +133 -0
- package/dist/core/plugin-bridge-connector.d.ts.map +1 -0
- package/dist/core/plugin-bridge-connector.js +155 -0
- package/dist/core/plugin-bridge-connector.js.map +1 -0
- package/dist/core/plugin-bridge-server.d.ts +42 -0
- package/dist/core/plugin-bridge-server.d.ts.map +1 -0
- package/dist/core/plugin-bridge-server.js +175 -0
- package/dist/core/plugin-bridge-server.js.map +1 -0
- package/dist/core/snippet-injector.d.ts +24 -0
- package/dist/core/snippet-injector.d.ts.map +1 -0
- package/dist/core/snippet-injector.js +97 -0
- package/dist/core/snippet-injector.js.map +1 -0
- package/dist/core/types/enriched.d.ts +213 -0
- package/dist/core/types/enriched.d.ts.map +1 -0
- package/dist/core/types/enriched.js +6 -0
- package/dist/core/types/enriched.js.map +1 -0
- package/dist/core/types/index.d.ts +116 -0
- package/dist/core/types/index.d.ts.map +1 -0
- package/dist/core/types/index.js +5 -0
- package/dist/core/types/index.js.map +1 -0
- package/dist/local-plugin-only.d.ts +13 -0
- package/dist/local-plugin-only.d.ts.map +1 -0
- package/dist/local-plugin-only.js +567 -0
- package/dist/local-plugin-only.js.map +1 -0
- package/dist/local.d.ts +73 -0
- package/dist/local.d.ts.map +1 -0
- package/dist/local.js +2466 -0
- package/dist/local.js.map +1 -0
- package/f-mcp-plugin/README.md +280 -0
- package/f-mcp-plugin/code.js +2222 -0
- package/f-mcp-plugin/manifest.json +14 -0
- package/f-mcp-plugin/ui.html +877 -0
- package/package.json +82 -0
|
@@ -0,0 +1,311 @@
|
|
|
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
|
+
}
|