@mp3wizard/figma-console-mcp 1.14.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/LICENSE +21 -0
- package/README.md +816 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.js +278 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts +29 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.js +358 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.js +342 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.js +231 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/engine.d.ts +27 -0
- package/dist/apps/design-system-dashboard/scoring/engine.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/engine.js +93 -0
- package/dist/apps/design-system-dashboard/scoring/engine.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.js +309 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.js +350 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/types.d.ts +89 -0
- package/dist/apps/design-system-dashboard/scoring/types.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/types.js +41 -0
- package/dist/apps/design-system-dashboard/scoring/types.js.map +1 -0
- package/dist/apps/design-system-dashboard/server.d.ts +24 -0
- package/dist/apps/design-system-dashboard/server.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/server.js +160 -0
- package/dist/apps/design-system-dashboard/server.js.map +1 -0
- package/dist/apps/token-browser/server.d.ts +26 -0
- package/dist/apps/token-browser/server.d.ts.map +1 -0
- package/dist/apps/token-browser/server.js +137 -0
- package/dist/apps/token-browser/server.js.map +1 -0
- package/dist/browser/base.d.ts +58 -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 +87 -0
- package/dist/browser/local.d.ts.map +1 -0
- package/dist/browser/local.js +318 -0
- package/dist/browser/local.js.map +1 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/accessibility.js +277 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/component-metadata.js +357 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/consistency.js +341 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/coverage.js +230 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/engine.js +92 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/naming-semantics.js +308 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/token-architecture.js +349 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/types.js +40 -0
- package/dist/cloudflare/apps/design-system-dashboard/server.js +159 -0
- package/dist/cloudflare/apps/token-browser/server.js +136 -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/cloud-websocket-connector.js +267 -0
- package/dist/cloudflare/core/cloud-websocket-relay.js +199 -0
- package/dist/cloudflare/core/comment-tools.js +292 -0
- package/dist/cloudflare/core/config.js +161 -0
- package/dist/cloudflare/core/console-monitor.js +427 -0
- package/dist/cloudflare/core/design-code-tools.js +2504 -0
- package/dist/cloudflare/core/design-system-manifest.js +260 -0
- package/dist/cloudflare/core/design-system-tools.js +863 -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 +409 -0
- package/dist/cloudflare/core/figma-connector.js +7 -0
- package/dist/cloudflare/core/figma-desktop-connector.js +1184 -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 +2947 -0
- package/dist/cloudflare/core/logger.js +53 -0
- package/dist/cloudflare/core/port-discovery.js +282 -0
- package/dist/cloudflare/core/snippet-injector.js +96 -0
- package/dist/cloudflare/core/types/design-code.js +4 -0
- package/dist/cloudflare/core/types/enriched.js +5 -0
- package/dist/cloudflare/core/types/index.js +4 -0
- package/dist/cloudflare/core/websocket-connector.js +256 -0
- package/dist/cloudflare/core/websocket-server.js +646 -0
- package/dist/cloudflare/core/write-tools.js +2091 -0
- package/dist/cloudflare/index.js +2899 -0
- package/dist/cloudflare/test-browser.js +88 -0
- package/dist/core/comment-tools.d.ts +11 -0
- package/dist/core/comment-tools.d.ts.map +1 -0
- package/dist/core/comment-tools.js +293 -0
- package/dist/core/comment-tools.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 +162 -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-code-tools.d.ts +127 -0
- package/dist/core/design-code-tools.d.ts.map +1 -0
- package/dist/core/design-code-tools.js +2505 -0
- package/dist/core/design-code-tools.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/design-system-tools.d.ts +17 -0
- package/dist/core/design-system-tools.d.ts.map +1 -0
- package/dist/core/design-system-tools.js +864 -0
- package/dist/core/design-system-tools.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 +201 -0
- package/dist/core/figma-api.d.ts.map +1 -0
- package/dist/core/figma-api.js +410 -0
- package/dist/core/figma-api.js.map +1 -0
- package/dist/core/figma-connector.d.ts +48 -0
- package/dist/core/figma-connector.d.ts.map +1 -0
- package/dist/core/figma-connector.js +8 -0
- package/dist/core/figma-connector.js.map +1 -0
- package/dist/core/figma-desktop-connector.d.ts +265 -0
- package/dist/core/figma-desktop-connector.d.ts.map +1 -0
- package/dist/core/figma-desktop-connector.js +1184 -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 +23 -0
- package/dist/core/figma-tools.d.ts.map +1 -0
- package/dist/core/figma-tools.js +2948 -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/port-discovery.d.ts +110 -0
- package/dist/core/port-discovery.d.ts.map +1 -0
- package/dist/core/port-discovery.js +283 -0
- package/dist/core/port-discovery.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/design-code.d.ts +262 -0
- package/dist/core/types/design-code.d.ts.map +1 -0
- package/dist/core/types/design-code.js +5 -0
- package/dist/core/types/design-code.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 +112 -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/core/websocket-connector.d.ts +55 -0
- package/dist/core/websocket-connector.d.ts.map +1 -0
- package/dist/core/websocket-connector.js +257 -0
- package/dist/core/websocket-connector.js.map +1 -0
- package/dist/core/websocket-server.d.ts +191 -0
- package/dist/core/websocket-server.d.ts.map +1 -0
- package/dist/core/websocket-server.js +647 -0
- package/dist/core/websocket-server.js.map +1 -0
- package/dist/core/write-tools.d.ts +7 -0
- package/dist/core/write-tools.d.ts.map +1 -0
- package/dist/core/write-tools.js +2092 -0
- package/dist/core/write-tools.js.map +1 -0
- package/dist/local.d.ts +84 -0
- package/dist/local.d.ts.map +1 -0
- package/dist/local.js +5039 -0
- package/dist/local.js.map +1 -0
- package/figma-desktop-bridge/README.md +313 -0
- package/figma-desktop-bridge/code.js +2818 -0
- package/figma-desktop-bridge/manifest.json +67 -0
- package/figma-desktop-bridge/ui.html +1236 -0
- package/package.json +87 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma Reconstruction Spec
|
|
3
|
+
*
|
|
4
|
+
* Generates node tree construction specifications compatible with the
|
|
5
|
+
* Figma Component Reconstructor plugin. This format differs from metadata
|
|
6
|
+
* export by providing all properties needed to programmatically recreate
|
|
7
|
+
* components in Figma.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Convert Figma paint array to reconstruction spec format
|
|
11
|
+
*/
|
|
12
|
+
export function convertFills(fills) {
|
|
13
|
+
if (!fills || fills === 'mixed')
|
|
14
|
+
return [];
|
|
15
|
+
return fills
|
|
16
|
+
.filter((fill) => fill.visible !== false)
|
|
17
|
+
.map((fill) => {
|
|
18
|
+
if (fill.type === 'SOLID') {
|
|
19
|
+
return {
|
|
20
|
+
type: 'SOLID',
|
|
21
|
+
color: {
|
|
22
|
+
r: fill.color?.r ?? 0,
|
|
23
|
+
g: fill.color?.g ?? 0,
|
|
24
|
+
b: fill.color?.b ?? 0,
|
|
25
|
+
a: fill.opacity ?? 1,
|
|
26
|
+
},
|
|
27
|
+
opacity: fill.opacity ?? 1,
|
|
28
|
+
visible: fill.visible ?? true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (fill.type.startsWith('GRADIENT')) {
|
|
32
|
+
return {
|
|
33
|
+
type: fill.type,
|
|
34
|
+
gradientStops: fill.gradientStops?.map((stop) => ({
|
|
35
|
+
color: {
|
|
36
|
+
r: stop.color?.r ?? 0,
|
|
37
|
+
g: stop.color?.g ?? 0,
|
|
38
|
+
b: stop.color?.b ?? 0,
|
|
39
|
+
a: stop.color?.a ?? 1,
|
|
40
|
+
},
|
|
41
|
+
position: stop.position ?? 0,
|
|
42
|
+
})) || [],
|
|
43
|
+
opacity: fill.opacity ?? 1,
|
|
44
|
+
visible: fill.visible ?? true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (fill.type === 'IMAGE') {
|
|
48
|
+
return {
|
|
49
|
+
type: 'IMAGE',
|
|
50
|
+
scaleMode: fill.scaleMode || 'FILL',
|
|
51
|
+
imageRef: fill.imageRef,
|
|
52
|
+
opacity: fill.opacity ?? 1,
|
|
53
|
+
visible: fill.visible ?? true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Default fallback
|
|
57
|
+
return null;
|
|
58
|
+
})
|
|
59
|
+
.filter((f) => f !== null);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Convert Figma stroke array to reconstruction spec format
|
|
63
|
+
*/
|
|
64
|
+
export function convertStrokes(strokes) {
|
|
65
|
+
// Strokes use same format as fills
|
|
66
|
+
return convertFills(strokes);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Convert REST API constraint values to Plugin API constraint values
|
|
70
|
+
* REST API: LEFT, RIGHT, TOP, BOTTOM, CENTER, SCALE
|
|
71
|
+
* Plugin API: MIN, MAX, CENTER, STRETCH, SCALE
|
|
72
|
+
*/
|
|
73
|
+
function convertConstraintValue(value) {
|
|
74
|
+
const mapping = {
|
|
75
|
+
'LEFT': 'MIN',
|
|
76
|
+
'RIGHT': 'MAX',
|
|
77
|
+
'TOP': 'MIN',
|
|
78
|
+
'BOTTOM': 'MAX',
|
|
79
|
+
'CENTER': 'CENTER',
|
|
80
|
+
'STRETCH': 'STRETCH',
|
|
81
|
+
'SCALE': 'SCALE',
|
|
82
|
+
};
|
|
83
|
+
return mapping[value] || value;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Convert Figma effects array to reconstruction spec format
|
|
87
|
+
*/
|
|
88
|
+
export function convertEffects(effects) {
|
|
89
|
+
if (!effects || effects === 'mixed')
|
|
90
|
+
return [];
|
|
91
|
+
return effects
|
|
92
|
+
.filter((effect) => effect.visible !== false)
|
|
93
|
+
.map((effect) => {
|
|
94
|
+
if (effect.type === 'DROP_SHADOW' || effect.type === 'INNER_SHADOW') {
|
|
95
|
+
return {
|
|
96
|
+
type: effect.type,
|
|
97
|
+
color: {
|
|
98
|
+
r: effect.color?.r ?? 0,
|
|
99
|
+
g: effect.color?.g ?? 0,
|
|
100
|
+
b: effect.color?.b ?? 0,
|
|
101
|
+
a: effect.color?.a ?? 1,
|
|
102
|
+
},
|
|
103
|
+
offset: effect.offset || { x: 0, y: 0 },
|
|
104
|
+
radius: effect.radius ?? 0,
|
|
105
|
+
spread: effect.spread,
|
|
106
|
+
visible: effect.visible ?? true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (effect.type === 'LAYER_BLUR' || effect.type === 'BACKGROUND_BLUR') {
|
|
110
|
+
return {
|
|
111
|
+
type: effect.type,
|
|
112
|
+
radius: effect.radius ?? 0,
|
|
113
|
+
visible: effect.visible ?? true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
})
|
|
118
|
+
.filter((e) => e !== null);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Recursively extract node specification for reconstruction
|
|
122
|
+
*/
|
|
123
|
+
export function extractNodeSpec(node) {
|
|
124
|
+
const spec = {
|
|
125
|
+
name: node.name,
|
|
126
|
+
type: node.type,
|
|
127
|
+
};
|
|
128
|
+
// INSTANCE → FRAME conversion for plugin compatibility
|
|
129
|
+
// Plugin cannot create instance nodes, so we convert them to frames
|
|
130
|
+
// This preserves visual properties and children for the "sketchpad" workflow
|
|
131
|
+
if (spec.type === 'INSTANCE') {
|
|
132
|
+
spec.type = 'FRAME';
|
|
133
|
+
// All visual properties (fills, strokes, layout) will be copied from the instance
|
|
134
|
+
// Children will be processed recursively and also converted if needed
|
|
135
|
+
}
|
|
136
|
+
// Position - provide defaults if missing
|
|
137
|
+
if ('x' in node && typeof node.x === 'number') {
|
|
138
|
+
spec.x = node.x;
|
|
139
|
+
}
|
|
140
|
+
else if (node.type !== 'GROUP' && node.type !== 'SECTION') {
|
|
141
|
+
spec.x = 0;
|
|
142
|
+
}
|
|
143
|
+
if ('y' in node && typeof node.y === 'number') {
|
|
144
|
+
spec.y = node.y;
|
|
145
|
+
}
|
|
146
|
+
else if (node.type !== 'GROUP' && node.type !== 'SECTION') {
|
|
147
|
+
spec.y = 0;
|
|
148
|
+
}
|
|
149
|
+
// Layout sizing for children in auto-layout parents
|
|
150
|
+
// These properties tell the plugin HOW the child should size itself (HUG content vs FIXED vs FILL)
|
|
151
|
+
if ('layoutSizingHorizontal' in node) {
|
|
152
|
+
spec.layoutSizingHorizontal = node.layoutSizingHorizontal;
|
|
153
|
+
}
|
|
154
|
+
if ('layoutSizingVertical' in node) {
|
|
155
|
+
spec.layoutSizingVertical = node.layoutSizingVertical;
|
|
156
|
+
}
|
|
157
|
+
// Dimensions - required for most node types to be reconstructable
|
|
158
|
+
// IMPORTANT: Skip explicit dimensions for children with HUG sizing in auto-layout
|
|
159
|
+
// The plugin will calculate dimensions based on sizing mode + content
|
|
160
|
+
const hasHugSizing = node.layoutSizingHorizontal === 'HUG' || node.layoutSizingVertical === 'HUG';
|
|
161
|
+
const isParentNode = node.type === 'COMPONENT' || node.type === 'FRAME' || node.type === 'INSTANCE';
|
|
162
|
+
const skipDimensions = hasHugSizing && !isParentNode;
|
|
163
|
+
if (!skipDimensions) {
|
|
164
|
+
// Check both direct properties (Desktop Bridge) and absoluteBoundingBox (REST API)
|
|
165
|
+
if ('width' in node && typeof node.width === 'number') {
|
|
166
|
+
spec.width = node.width;
|
|
167
|
+
}
|
|
168
|
+
else if ('absoluteBoundingBox' in node && node.absoluteBoundingBox && typeof node.absoluteBoundingBox.width === 'number') {
|
|
169
|
+
spec.width = node.absoluteBoundingBox.width;
|
|
170
|
+
}
|
|
171
|
+
else if (node.type !== 'GROUP' && node.type !== 'SECTION') {
|
|
172
|
+
// Default width for nodes that need it
|
|
173
|
+
spec.width = node.type === 'TEXT' ? 100 :
|
|
174
|
+
node.type === 'COMPONENT_SET' ? 200 :
|
|
175
|
+
node.type === 'ELLIPSE' ? 8 : 50;
|
|
176
|
+
}
|
|
177
|
+
if ('height' in node && typeof node.height === 'number') {
|
|
178
|
+
spec.height = node.height;
|
|
179
|
+
}
|
|
180
|
+
else if ('absoluteBoundingBox' in node && node.absoluteBoundingBox && typeof node.absoluteBoundingBox.height === 'number') {
|
|
181
|
+
spec.height = node.absoluteBoundingBox.height;
|
|
182
|
+
}
|
|
183
|
+
else if (node.type !== 'GROUP' && node.type !== 'SECTION') {
|
|
184
|
+
// Default height for nodes that need it
|
|
185
|
+
spec.height = node.type === 'TEXT' ? 20 :
|
|
186
|
+
node.type === 'COMPONENT_SET' ? 100 :
|
|
187
|
+
node.type === 'ELLIPSE' ? 8 : 50;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Visual properties (only include what plugin spec needs)
|
|
191
|
+
if ('opacity' in node && typeof node.opacity === 'number' && node.opacity !== 1) {
|
|
192
|
+
spec.opacity = node.opacity;
|
|
193
|
+
}
|
|
194
|
+
// Fills (only include if present and non-empty)
|
|
195
|
+
if ('fills' in node && node.fills !== 'mixed') {
|
|
196
|
+
const convertedFills = convertFills(node.fills);
|
|
197
|
+
if (convertedFills.length > 0) {
|
|
198
|
+
spec.fills = convertedFills;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Strokes (only include if present and non-empty)
|
|
202
|
+
if ('strokes' in node && node.strokes !== 'mixed') {
|
|
203
|
+
const convertedStrokes = convertStrokes(node.strokes);
|
|
204
|
+
if (convertedStrokes.length > 0) {
|
|
205
|
+
spec.strokes = convertedStrokes;
|
|
206
|
+
// Only include stroke weight if there are strokes
|
|
207
|
+
if ('strokeWeight' in node && typeof node.strokeWeight === 'number') {
|
|
208
|
+
spec.strokeWeight = node.strokeWeight;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Effects (only include if present and non-empty)
|
|
213
|
+
if ('effects' in node && node.effects !== 'mixed') {
|
|
214
|
+
const convertedEffects = convertEffects(node.effects);
|
|
215
|
+
if (convertedEffects.length > 0) {
|
|
216
|
+
spec.effects = convertedEffects;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Corner radius
|
|
220
|
+
if ('cornerRadius' in node && typeof node.cornerRadius === 'number') {
|
|
221
|
+
spec.cornerRadius = node.cornerRadius;
|
|
222
|
+
}
|
|
223
|
+
if ('rectangleCornerRadii' in node && Array.isArray(node.rectangleCornerRadii)) {
|
|
224
|
+
spec.rectangleCornerRadii = node.rectangleCornerRadii;
|
|
225
|
+
}
|
|
226
|
+
// Layout properties (for FRAME, COMPONENT, INSTANCE)
|
|
227
|
+
// Plugin spec requires: layoutMode, primaryAxisSizingMode, counterAxisSizingMode, itemSpacing, padding*
|
|
228
|
+
if ('layoutMode' in node && node.layoutMode !== 'NONE') {
|
|
229
|
+
spec.layoutMode = node.layoutMode;
|
|
230
|
+
// Sizing modes (REQUIRED for plugin spec)
|
|
231
|
+
if ('primaryAxisSizingMode' in node)
|
|
232
|
+
spec.primaryAxisSizingMode = node.primaryAxisSizingMode;
|
|
233
|
+
if ('counterAxisSizingMode' in node)
|
|
234
|
+
spec.counterAxisSizingMode = node.counterAxisSizingMode;
|
|
235
|
+
// Spacing
|
|
236
|
+
if ('itemSpacing' in node)
|
|
237
|
+
spec.itemSpacing = node.itemSpacing;
|
|
238
|
+
// Padding (all four sides)
|
|
239
|
+
if ('paddingLeft' in node)
|
|
240
|
+
spec.paddingLeft = node.paddingLeft;
|
|
241
|
+
if ('paddingRight' in node)
|
|
242
|
+
spec.paddingRight = node.paddingRight;
|
|
243
|
+
if ('paddingTop' in node)
|
|
244
|
+
spec.paddingTop = node.paddingTop;
|
|
245
|
+
if ('paddingBottom' in node)
|
|
246
|
+
spec.paddingBottom = node.paddingBottom;
|
|
247
|
+
}
|
|
248
|
+
// TEXT node specific properties
|
|
249
|
+
// REST API returns text styling in a 'style' object
|
|
250
|
+
if (node.type === 'TEXT') {
|
|
251
|
+
if ('characters' in node)
|
|
252
|
+
spec.characters = node.characters;
|
|
253
|
+
// Check both direct properties (Plugin API) and style object (REST API)
|
|
254
|
+
if ('fontSize' in node && typeof node.fontSize === 'number') {
|
|
255
|
+
spec.fontSize = node.fontSize;
|
|
256
|
+
}
|
|
257
|
+
else if (node.style?.fontSize && typeof node.style.fontSize === 'number') {
|
|
258
|
+
spec.fontSize = node.style.fontSize;
|
|
259
|
+
}
|
|
260
|
+
if ('fontName' in node) {
|
|
261
|
+
spec.fontName = node.fontName;
|
|
262
|
+
}
|
|
263
|
+
else if (node.style?.fontFamily && node.style?.fontWeight) {
|
|
264
|
+
spec.fontName = {
|
|
265
|
+
family: node.style.fontFamily,
|
|
266
|
+
style: node.style.fontWeight
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if ('textAlignHorizontal' in node) {
|
|
270
|
+
spec.textAlignHorizontal = node.textAlignHorizontal;
|
|
271
|
+
}
|
|
272
|
+
else if (node.style?.textAlignHorizontal) {
|
|
273
|
+
spec.textAlignHorizontal = node.style.textAlignHorizontal;
|
|
274
|
+
}
|
|
275
|
+
if ('textAlignVertical' in node) {
|
|
276
|
+
spec.textAlignVertical = node.textAlignVertical;
|
|
277
|
+
}
|
|
278
|
+
else if (node.style?.textAlignVertical) {
|
|
279
|
+
spec.textAlignVertical = node.style.textAlignVertical;
|
|
280
|
+
}
|
|
281
|
+
if ('letterSpacing' in node) {
|
|
282
|
+
spec.letterSpacing = node.letterSpacing;
|
|
283
|
+
}
|
|
284
|
+
else if (node.style?.letterSpacing) {
|
|
285
|
+
spec.letterSpacing = node.style.letterSpacing;
|
|
286
|
+
}
|
|
287
|
+
if ('lineHeight' in node) {
|
|
288
|
+
spec.lineHeight = node.lineHeight;
|
|
289
|
+
}
|
|
290
|
+
else if (node.style?.lineHeight) {
|
|
291
|
+
spec.lineHeight = node.style.lineHeight;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Variant properties (for COMPONENT in a COMPONENT_SET)
|
|
295
|
+
if (node.type === 'COMPONENT' && 'variantProperties' in node && node.variantProperties) {
|
|
296
|
+
spec.variantProperties = node.variantProperties;
|
|
297
|
+
}
|
|
298
|
+
// Children (recursive)
|
|
299
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
300
|
+
spec.children = node.children.map((child) => extractNodeSpec(child));
|
|
301
|
+
}
|
|
302
|
+
return spec;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Validate that the reconstruction spec has required fields
|
|
306
|
+
*/
|
|
307
|
+
export function validateReconstructionSpec(spec) {
|
|
308
|
+
const errors = [];
|
|
309
|
+
// Required fields
|
|
310
|
+
if (!spec.name || typeof spec.name !== 'string') {
|
|
311
|
+
errors.push('Missing or invalid required field: name');
|
|
312
|
+
}
|
|
313
|
+
if (!spec.type || typeof spec.type !== 'string') {
|
|
314
|
+
errors.push('Missing or invalid required field: type');
|
|
315
|
+
}
|
|
316
|
+
// Valid node types
|
|
317
|
+
const validTypes = [
|
|
318
|
+
'FRAME',
|
|
319
|
+
'COMPONENT',
|
|
320
|
+
'COMPONENT_SET',
|
|
321
|
+
'INSTANCE',
|
|
322
|
+
'TEXT',
|
|
323
|
+
'RECTANGLE',
|
|
324
|
+
'ELLIPSE',
|
|
325
|
+
'POLYGON',
|
|
326
|
+
'STAR',
|
|
327
|
+
'VECTOR',
|
|
328
|
+
'LINE',
|
|
329
|
+
'GROUP',
|
|
330
|
+
'SECTION',
|
|
331
|
+
'SLICE',
|
|
332
|
+
'BOOLEAN_OPERATION',
|
|
333
|
+
];
|
|
334
|
+
if (spec.type && !validTypes.includes(spec.type)) {
|
|
335
|
+
errors.push(`Invalid node type: ${spec.type}. Must be one of: ${validTypes.join(', ')}`);
|
|
336
|
+
}
|
|
337
|
+
// Validate dimensions if present
|
|
338
|
+
if ('width' in spec && (typeof spec.width !== 'number' || spec.width < 0)) {
|
|
339
|
+
errors.push('Invalid width: must be a non-negative number');
|
|
340
|
+
}
|
|
341
|
+
if ('height' in spec && (typeof spec.height !== 'number' || spec.height < 0)) {
|
|
342
|
+
errors.push('Invalid height: must be a non-negative number');
|
|
343
|
+
}
|
|
344
|
+
// Validate opacity if present
|
|
345
|
+
if ('opacity' in spec && (typeof spec.opacity !== 'number' || spec.opacity < 0 || spec.opacity > 1)) {
|
|
346
|
+
errors.push('Invalid opacity: must be a number between 0 and 1');
|
|
347
|
+
}
|
|
348
|
+
// Validate colors in fills
|
|
349
|
+
if (spec.fills && Array.isArray(spec.fills)) {
|
|
350
|
+
spec.fills.forEach((fill, index) => {
|
|
351
|
+
if (fill.type === 'SOLID' && fill.color) {
|
|
352
|
+
const { r, g, b, a } = fill.color;
|
|
353
|
+
if (typeof r !== 'number' || r < 0 || r > 1 ||
|
|
354
|
+
typeof g !== 'number' || g < 0 || g > 1 ||
|
|
355
|
+
typeof b !== 'number' || b < 0 || b > 1 ||
|
|
356
|
+
typeof a !== 'number' || a < 0 || a > 1) {
|
|
357
|
+
errors.push(`Invalid color in fills[${index}]: RGB values must be between 0 and 1`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
// Recursively validate children
|
|
363
|
+
if (spec.children && Array.isArray(spec.children)) {
|
|
364
|
+
spec.children.forEach((child, index) => {
|
|
365
|
+
const childValidation = validateReconstructionSpec(child);
|
|
366
|
+
if (!childValidation.valid) {
|
|
367
|
+
errors.push(`Errors in children[${index}]: ${childValidation.errors.join(', ')}`);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
valid: errors.length === 0,
|
|
373
|
+
errors,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Extract a specific variant from a COMPONENT_SET by name
|
|
378
|
+
*/
|
|
379
|
+
export function extractVariant(componentSet, variantName) {
|
|
380
|
+
if (!componentSet.children || !Array.isArray(componentSet.children)) {
|
|
381
|
+
throw new Error('Invalid COMPONENT_SET: no children array');
|
|
382
|
+
}
|
|
383
|
+
const variant = componentSet.children.find((child) => child.type === 'COMPONENT' && child.name === variantName);
|
|
384
|
+
if (!variant) {
|
|
385
|
+
const availableVariants = componentSet.children
|
|
386
|
+
.filter((c) => c.type === 'COMPONENT')
|
|
387
|
+
.map((c) => c.name);
|
|
388
|
+
throw new Error(`Variant "${variantName}" not found. Available variants: ${availableVariants.join(', ')}`);
|
|
389
|
+
}
|
|
390
|
+
return extractNodeSpec(variant);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Get list of available variants in a COMPONENT_SET
|
|
394
|
+
*/
|
|
395
|
+
export function listVariants(componentSet) {
|
|
396
|
+
if (!componentSet.children || !Array.isArray(componentSet.children)) {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
return componentSet.children
|
|
400
|
+
.filter((child) => child.type === 'COMPONENT')
|
|
401
|
+
.map((child) => child.name);
|
|
402
|
+
}
|