@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.
Files changed (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +816 -0
  3. package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts +14 -0
  4. package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts.map +1 -0
  5. package/dist/apps/design-system-dashboard/scoring/accessibility.js +278 -0
  6. package/dist/apps/design-system-dashboard/scoring/accessibility.js.map +1 -0
  7. package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts +29 -0
  8. package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts.map +1 -0
  9. package/dist/apps/design-system-dashboard/scoring/component-metadata.js +358 -0
  10. package/dist/apps/design-system-dashboard/scoring/component-metadata.js.map +1 -0
  11. package/dist/apps/design-system-dashboard/scoring/consistency.d.ts +14 -0
  12. package/dist/apps/design-system-dashboard/scoring/consistency.d.ts.map +1 -0
  13. package/dist/apps/design-system-dashboard/scoring/consistency.js +342 -0
  14. package/dist/apps/design-system-dashboard/scoring/consistency.js.map +1 -0
  15. package/dist/apps/design-system-dashboard/scoring/coverage.d.ts +14 -0
  16. package/dist/apps/design-system-dashboard/scoring/coverage.d.ts.map +1 -0
  17. package/dist/apps/design-system-dashboard/scoring/coverage.js +231 -0
  18. package/dist/apps/design-system-dashboard/scoring/coverage.js.map +1 -0
  19. package/dist/apps/design-system-dashboard/scoring/engine.d.ts +27 -0
  20. package/dist/apps/design-system-dashboard/scoring/engine.d.ts.map +1 -0
  21. package/dist/apps/design-system-dashboard/scoring/engine.js +93 -0
  22. package/dist/apps/design-system-dashboard/scoring/engine.js.map +1 -0
  23. package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts +14 -0
  24. package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts.map +1 -0
  25. package/dist/apps/design-system-dashboard/scoring/naming-semantics.js +309 -0
  26. package/dist/apps/design-system-dashboard/scoring/naming-semantics.js.map +1 -0
  27. package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts +14 -0
  28. package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts.map +1 -0
  29. package/dist/apps/design-system-dashboard/scoring/token-architecture.js +350 -0
  30. package/dist/apps/design-system-dashboard/scoring/token-architecture.js.map +1 -0
  31. package/dist/apps/design-system-dashboard/scoring/types.d.ts +89 -0
  32. package/dist/apps/design-system-dashboard/scoring/types.d.ts.map +1 -0
  33. package/dist/apps/design-system-dashboard/scoring/types.js +41 -0
  34. package/dist/apps/design-system-dashboard/scoring/types.js.map +1 -0
  35. package/dist/apps/design-system-dashboard/server.d.ts +24 -0
  36. package/dist/apps/design-system-dashboard/server.d.ts.map +1 -0
  37. package/dist/apps/design-system-dashboard/server.js +160 -0
  38. package/dist/apps/design-system-dashboard/server.js.map +1 -0
  39. package/dist/apps/token-browser/server.d.ts +26 -0
  40. package/dist/apps/token-browser/server.d.ts.map +1 -0
  41. package/dist/apps/token-browser/server.js +137 -0
  42. package/dist/apps/token-browser/server.js.map +1 -0
  43. package/dist/browser/base.d.ts +58 -0
  44. package/dist/browser/base.d.ts.map +1 -0
  45. package/dist/browser/base.js +6 -0
  46. package/dist/browser/base.js.map +1 -0
  47. package/dist/browser/local.d.ts +87 -0
  48. package/dist/browser/local.d.ts.map +1 -0
  49. package/dist/browser/local.js +318 -0
  50. package/dist/browser/local.js.map +1 -0
  51. package/dist/cloudflare/apps/design-system-dashboard/scoring/accessibility.js +277 -0
  52. package/dist/cloudflare/apps/design-system-dashboard/scoring/component-metadata.js +357 -0
  53. package/dist/cloudflare/apps/design-system-dashboard/scoring/consistency.js +341 -0
  54. package/dist/cloudflare/apps/design-system-dashboard/scoring/coverage.js +230 -0
  55. package/dist/cloudflare/apps/design-system-dashboard/scoring/engine.js +92 -0
  56. package/dist/cloudflare/apps/design-system-dashboard/scoring/naming-semantics.js +308 -0
  57. package/dist/cloudflare/apps/design-system-dashboard/scoring/token-architecture.js +349 -0
  58. package/dist/cloudflare/apps/design-system-dashboard/scoring/types.js +40 -0
  59. package/dist/cloudflare/apps/design-system-dashboard/server.js +159 -0
  60. package/dist/cloudflare/apps/token-browser/server.js +136 -0
  61. package/dist/cloudflare/browser/base.js +5 -0
  62. package/dist/cloudflare/browser/cloudflare.js +156 -0
  63. package/dist/cloudflare/browser-manager.js +157 -0
  64. package/dist/cloudflare/core/cloud-websocket-connector.js +267 -0
  65. package/dist/cloudflare/core/cloud-websocket-relay.js +199 -0
  66. package/dist/cloudflare/core/comment-tools.js +292 -0
  67. package/dist/cloudflare/core/config.js +161 -0
  68. package/dist/cloudflare/core/console-monitor.js +427 -0
  69. package/dist/cloudflare/core/design-code-tools.js +2504 -0
  70. package/dist/cloudflare/core/design-system-manifest.js +260 -0
  71. package/dist/cloudflare/core/design-system-tools.js +863 -0
  72. package/dist/cloudflare/core/enrichment/enrichment-service.js +272 -0
  73. package/dist/cloudflare/core/enrichment/index.js +7 -0
  74. package/dist/cloudflare/core/enrichment/relationship-mapper.js +351 -0
  75. package/dist/cloudflare/core/enrichment/style-resolver.js +326 -0
  76. package/dist/cloudflare/core/figma-api.js +409 -0
  77. package/dist/cloudflare/core/figma-connector.js +7 -0
  78. package/dist/cloudflare/core/figma-desktop-connector.js +1184 -0
  79. package/dist/cloudflare/core/figma-reconstruction-spec.js +402 -0
  80. package/dist/cloudflare/core/figma-style-extractor.js +311 -0
  81. package/dist/cloudflare/core/figma-tools.js +2947 -0
  82. package/dist/cloudflare/core/logger.js +53 -0
  83. package/dist/cloudflare/core/port-discovery.js +282 -0
  84. package/dist/cloudflare/core/snippet-injector.js +96 -0
  85. package/dist/cloudflare/core/types/design-code.js +4 -0
  86. package/dist/cloudflare/core/types/enriched.js +5 -0
  87. package/dist/cloudflare/core/types/index.js +4 -0
  88. package/dist/cloudflare/core/websocket-connector.js +256 -0
  89. package/dist/cloudflare/core/websocket-server.js +646 -0
  90. package/dist/cloudflare/core/write-tools.js +2091 -0
  91. package/dist/cloudflare/index.js +2899 -0
  92. package/dist/cloudflare/test-browser.js +88 -0
  93. package/dist/core/comment-tools.d.ts +11 -0
  94. package/dist/core/comment-tools.d.ts.map +1 -0
  95. package/dist/core/comment-tools.js +293 -0
  96. package/dist/core/comment-tools.js.map +1 -0
  97. package/dist/core/config.d.ts +17 -0
  98. package/dist/core/config.d.ts.map +1 -0
  99. package/dist/core/config.js +162 -0
  100. package/dist/core/config.js.map +1 -0
  101. package/dist/core/console-monitor.d.ts +82 -0
  102. package/dist/core/console-monitor.d.ts.map +1 -0
  103. package/dist/core/console-monitor.js +428 -0
  104. package/dist/core/console-monitor.js.map +1 -0
  105. package/dist/core/design-code-tools.d.ts +127 -0
  106. package/dist/core/design-code-tools.d.ts.map +1 -0
  107. package/dist/core/design-code-tools.js +2505 -0
  108. package/dist/core/design-code-tools.js.map +1 -0
  109. package/dist/core/design-system-manifest.d.ts +272 -0
  110. package/dist/core/design-system-manifest.d.ts.map +1 -0
  111. package/dist/core/design-system-manifest.js +261 -0
  112. package/dist/core/design-system-manifest.js.map +1 -0
  113. package/dist/core/design-system-tools.d.ts +17 -0
  114. package/dist/core/design-system-tools.d.ts.map +1 -0
  115. package/dist/core/design-system-tools.js +864 -0
  116. package/dist/core/design-system-tools.js.map +1 -0
  117. package/dist/core/enrichment/enrichment-service.d.ts +52 -0
  118. package/dist/core/enrichment/enrichment-service.d.ts.map +1 -0
  119. package/dist/core/enrichment/enrichment-service.js +273 -0
  120. package/dist/core/enrichment/enrichment-service.js.map +1 -0
  121. package/dist/core/enrichment/index.d.ts +8 -0
  122. package/dist/core/enrichment/index.d.ts.map +1 -0
  123. package/dist/core/enrichment/index.js +8 -0
  124. package/dist/core/enrichment/index.js.map +1 -0
  125. package/dist/core/enrichment/relationship-mapper.d.ts +106 -0
  126. package/dist/core/enrichment/relationship-mapper.d.ts.map +1 -0
  127. package/dist/core/enrichment/relationship-mapper.js +352 -0
  128. package/dist/core/enrichment/relationship-mapper.js.map +1 -0
  129. package/dist/core/enrichment/style-resolver.d.ts +80 -0
  130. package/dist/core/enrichment/style-resolver.d.ts.map +1 -0
  131. package/dist/core/enrichment/style-resolver.js +327 -0
  132. package/dist/core/enrichment/style-resolver.js.map +1 -0
  133. package/dist/core/figma-api.d.ts +201 -0
  134. package/dist/core/figma-api.d.ts.map +1 -0
  135. package/dist/core/figma-api.js +410 -0
  136. package/dist/core/figma-api.js.map +1 -0
  137. package/dist/core/figma-connector.d.ts +48 -0
  138. package/dist/core/figma-connector.d.ts.map +1 -0
  139. package/dist/core/figma-connector.js +8 -0
  140. package/dist/core/figma-connector.js.map +1 -0
  141. package/dist/core/figma-desktop-connector.d.ts +265 -0
  142. package/dist/core/figma-desktop-connector.d.ts.map +1 -0
  143. package/dist/core/figma-desktop-connector.js +1184 -0
  144. package/dist/core/figma-desktop-connector.js.map +1 -0
  145. package/dist/core/figma-reconstruction-spec.d.ts +166 -0
  146. package/dist/core/figma-reconstruction-spec.d.ts.map +1 -0
  147. package/dist/core/figma-reconstruction-spec.js +403 -0
  148. package/dist/core/figma-reconstruction-spec.js.map +1 -0
  149. package/dist/core/figma-style-extractor.d.ts +76 -0
  150. package/dist/core/figma-style-extractor.d.ts.map +1 -0
  151. package/dist/core/figma-style-extractor.js +312 -0
  152. package/dist/core/figma-style-extractor.js.map +1 -0
  153. package/dist/core/figma-tools.d.ts +23 -0
  154. package/dist/core/figma-tools.d.ts.map +1 -0
  155. package/dist/core/figma-tools.js +2948 -0
  156. package/dist/core/figma-tools.js.map +1 -0
  157. package/dist/core/logger.d.ts +22 -0
  158. package/dist/core/logger.d.ts.map +1 -0
  159. package/dist/core/logger.js +54 -0
  160. package/dist/core/logger.js.map +1 -0
  161. package/dist/core/port-discovery.d.ts +110 -0
  162. package/dist/core/port-discovery.d.ts.map +1 -0
  163. package/dist/core/port-discovery.js +283 -0
  164. package/dist/core/port-discovery.js.map +1 -0
  165. package/dist/core/snippet-injector.d.ts +24 -0
  166. package/dist/core/snippet-injector.d.ts.map +1 -0
  167. package/dist/core/snippet-injector.js +97 -0
  168. package/dist/core/snippet-injector.js.map +1 -0
  169. package/dist/core/types/design-code.d.ts +262 -0
  170. package/dist/core/types/design-code.d.ts.map +1 -0
  171. package/dist/core/types/design-code.js +5 -0
  172. package/dist/core/types/design-code.js.map +1 -0
  173. package/dist/core/types/enriched.d.ts +213 -0
  174. package/dist/core/types/enriched.d.ts.map +1 -0
  175. package/dist/core/types/enriched.js +6 -0
  176. package/dist/core/types/enriched.js.map +1 -0
  177. package/dist/core/types/index.d.ts +112 -0
  178. package/dist/core/types/index.d.ts.map +1 -0
  179. package/dist/core/types/index.js +5 -0
  180. package/dist/core/types/index.js.map +1 -0
  181. package/dist/core/websocket-connector.d.ts +55 -0
  182. package/dist/core/websocket-connector.d.ts.map +1 -0
  183. package/dist/core/websocket-connector.js +257 -0
  184. package/dist/core/websocket-connector.js.map +1 -0
  185. package/dist/core/websocket-server.d.ts +191 -0
  186. package/dist/core/websocket-server.d.ts.map +1 -0
  187. package/dist/core/websocket-server.js +647 -0
  188. package/dist/core/websocket-server.js.map +1 -0
  189. package/dist/core/write-tools.d.ts +7 -0
  190. package/dist/core/write-tools.d.ts.map +1 -0
  191. package/dist/core/write-tools.js +2092 -0
  192. package/dist/core/write-tools.js.map +1 -0
  193. package/dist/local.d.ts +84 -0
  194. package/dist/local.d.ts.map +1 -0
  195. package/dist/local.js +5039 -0
  196. package/dist/local.js.map +1 -0
  197. package/figma-desktop-bridge/README.md +313 -0
  198. package/figma-desktop-bridge/code.js +2818 -0
  199. package/figma-desktop-bridge/manifest.json +67 -0
  200. package/figma-desktop-bridge/ui.html +1236 -0
  201. 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
+ }