@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,863 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design System Kit Tool
|
|
3
|
+
* MCP tool that orchestrates existing Figma API tools to produce a structured
|
|
4
|
+
* design system specification — tokens, components, styles — in a single call.
|
|
5
|
+
*
|
|
6
|
+
* This enables AI code generation tools (Figma Make, v0, Cursor, Claude, etc.)
|
|
7
|
+
* to generate code with structural fidelity to the real design system.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { extractFileKey, formatVariables } from "./figma-api.js";
|
|
11
|
+
import { createChildLogger } from "./logger.js";
|
|
12
|
+
const logger = createChildLogger({ component: "design-system-tools" });
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Helpers
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Calculate JSON size in KB for response management
|
|
18
|
+
*/
|
|
19
|
+
function calculateSizeKB(data) {
|
|
20
|
+
return JSON.stringify(data).length / 1024;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Wrap a promise with a timeout to prevent indefinite hangs
|
|
24
|
+
*/
|
|
25
|
+
function withTimeout(promise, ms, label) {
|
|
26
|
+
return Promise.race([
|
|
27
|
+
promise,
|
|
28
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)),
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Convert Figma RGBA (0-1 range) to hex string
|
|
33
|
+
*/
|
|
34
|
+
function rgbaToHex(color) {
|
|
35
|
+
const r = Math.round(color.r * 255);
|
|
36
|
+
const g = Math.round(color.g * 255);
|
|
37
|
+
const b = Math.round(color.b * 255);
|
|
38
|
+
const hex = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
39
|
+
return hex.toUpperCase();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Extract a compact visual specification from a Figma node.
|
|
43
|
+
* Captures the essential CSS-equivalent properties an AI needs to reproduce the component.
|
|
44
|
+
*/
|
|
45
|
+
function extractVisualSpec(node) {
|
|
46
|
+
if (!node)
|
|
47
|
+
return undefined;
|
|
48
|
+
const spec = {};
|
|
49
|
+
let hasData = false;
|
|
50
|
+
// Fills → background colors/gradients
|
|
51
|
+
if (node.fills && Array.isArray(node.fills) && node.fills.length > 0) {
|
|
52
|
+
spec.fills = node.fills
|
|
53
|
+
.filter((f) => f.visible !== false)
|
|
54
|
+
.map((f) => {
|
|
55
|
+
const fill = { type: f.type };
|
|
56
|
+
if (f.color)
|
|
57
|
+
fill.color = rgbaToHex(f.color);
|
|
58
|
+
if (f.opacity !== undefined)
|
|
59
|
+
fill.opacity = f.opacity;
|
|
60
|
+
return fill;
|
|
61
|
+
});
|
|
62
|
+
if (spec.fills.length > 0)
|
|
63
|
+
hasData = true;
|
|
64
|
+
}
|
|
65
|
+
// Strokes → borders
|
|
66
|
+
if (node.strokes && Array.isArray(node.strokes) && node.strokes.length > 0) {
|
|
67
|
+
spec.strokes = node.strokes
|
|
68
|
+
.filter((s) => s.visible !== false)
|
|
69
|
+
.map((s) => {
|
|
70
|
+
const stroke = { type: s.type };
|
|
71
|
+
if (s.color)
|
|
72
|
+
stroke.color = rgbaToHex(s.color);
|
|
73
|
+
return stroke;
|
|
74
|
+
});
|
|
75
|
+
if (node.strokeWeight !== undefined)
|
|
76
|
+
spec.strokes.forEach((s) => s.weight = node.strokeWeight);
|
|
77
|
+
if (node.strokeAlign)
|
|
78
|
+
spec.strokes.forEach((s) => s.align = node.strokeAlign);
|
|
79
|
+
if (spec.strokes.length > 0)
|
|
80
|
+
hasData = true;
|
|
81
|
+
}
|
|
82
|
+
// Effects → shadows, blurs
|
|
83
|
+
if (node.effects && Array.isArray(node.effects) && node.effects.length > 0) {
|
|
84
|
+
spec.effects = node.effects
|
|
85
|
+
.filter((e) => e.visible !== false)
|
|
86
|
+
.map((e) => {
|
|
87
|
+
const effect = { type: e.type };
|
|
88
|
+
if (e.color)
|
|
89
|
+
effect.color = rgbaToHex(e.color);
|
|
90
|
+
if (e.offset)
|
|
91
|
+
effect.offset = e.offset;
|
|
92
|
+
if (e.radius !== undefined)
|
|
93
|
+
effect.radius = e.radius;
|
|
94
|
+
if (e.spread !== undefined)
|
|
95
|
+
effect.spread = e.spread;
|
|
96
|
+
return effect;
|
|
97
|
+
});
|
|
98
|
+
if (spec.effects.length > 0)
|
|
99
|
+
hasData = true;
|
|
100
|
+
}
|
|
101
|
+
// Corner radius
|
|
102
|
+
if (node.cornerRadius !== undefined && node.cornerRadius > 0) {
|
|
103
|
+
spec.cornerRadius = node.cornerRadius;
|
|
104
|
+
hasData = true;
|
|
105
|
+
}
|
|
106
|
+
if (node.rectangleCornerRadii) {
|
|
107
|
+
spec.rectangleCornerRadii = node.rectangleCornerRadii;
|
|
108
|
+
hasData = true;
|
|
109
|
+
}
|
|
110
|
+
// Opacity
|
|
111
|
+
if (node.opacity !== undefined && node.opacity < 1) {
|
|
112
|
+
spec.opacity = node.opacity;
|
|
113
|
+
hasData = true;
|
|
114
|
+
}
|
|
115
|
+
// Auto-layout → CSS flex equivalent
|
|
116
|
+
if (node.layoutMode && node.layoutMode !== "NONE") {
|
|
117
|
+
spec.layout = {
|
|
118
|
+
mode: node.layoutMode,
|
|
119
|
+
};
|
|
120
|
+
if (node.paddingTop !== undefined)
|
|
121
|
+
spec.layout.paddingTop = node.paddingTop;
|
|
122
|
+
if (node.paddingRight !== undefined)
|
|
123
|
+
spec.layout.paddingRight = node.paddingRight;
|
|
124
|
+
if (node.paddingBottom !== undefined)
|
|
125
|
+
spec.layout.paddingBottom = node.paddingBottom;
|
|
126
|
+
if (node.paddingLeft !== undefined)
|
|
127
|
+
spec.layout.paddingLeft = node.paddingLeft;
|
|
128
|
+
if (node.itemSpacing !== undefined)
|
|
129
|
+
spec.layout.itemSpacing = node.itemSpacing;
|
|
130
|
+
if (node.primaryAxisAlignItems)
|
|
131
|
+
spec.layout.primaryAxisAlign = node.primaryAxisAlignItems;
|
|
132
|
+
if (node.counterAxisAlignItems)
|
|
133
|
+
spec.layout.counterAxisAlign = node.counterAxisAlignItems;
|
|
134
|
+
hasData = true;
|
|
135
|
+
}
|
|
136
|
+
// Typography (for TEXT nodes)
|
|
137
|
+
if (node.type === "TEXT" && node.style) {
|
|
138
|
+
spec.typography = {};
|
|
139
|
+
const s = node.style;
|
|
140
|
+
if (s.fontFamily)
|
|
141
|
+
spec.typography.fontFamily = s.fontFamily;
|
|
142
|
+
if (s.fontSize)
|
|
143
|
+
spec.typography.fontSize = s.fontSize;
|
|
144
|
+
if (s.fontWeight)
|
|
145
|
+
spec.typography.fontWeight = s.fontWeight;
|
|
146
|
+
if (s.lineHeightPx)
|
|
147
|
+
spec.typography.lineHeight = s.lineHeightPx;
|
|
148
|
+
if (s.letterSpacing)
|
|
149
|
+
spec.typography.letterSpacing = s.letterSpacing;
|
|
150
|
+
if (s.textAlignHorizontal)
|
|
151
|
+
spec.typography.textAlignHorizontal = s.textAlignHorizontal;
|
|
152
|
+
hasData = true;
|
|
153
|
+
}
|
|
154
|
+
return hasData ? spec : undefined;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Extract visual specs from a component node and its first-level children.
|
|
158
|
+
* Returns a compact representation of the component's visual appearance.
|
|
159
|
+
*/
|
|
160
|
+
function extractComponentVisualData(node) {
|
|
161
|
+
if (!node)
|
|
162
|
+
return {};
|
|
163
|
+
const result = {};
|
|
164
|
+
const rootSpec = extractVisualSpec(node);
|
|
165
|
+
if (rootSpec)
|
|
166
|
+
result.visualSpec = rootSpec;
|
|
167
|
+
// Extract first-level children specs (the structural elements)
|
|
168
|
+
if (node.children && Array.isArray(node.children)) {
|
|
169
|
+
const childSpecs = [];
|
|
170
|
+
for (const child of node.children) {
|
|
171
|
+
const childInfo = {
|
|
172
|
+
name: child.name,
|
|
173
|
+
type: child.type,
|
|
174
|
+
};
|
|
175
|
+
const childVisual = extractVisualSpec(child);
|
|
176
|
+
if (childVisual)
|
|
177
|
+
childInfo.visualSpec = childVisual;
|
|
178
|
+
if (child.characters)
|
|
179
|
+
childInfo.characters = child.characters;
|
|
180
|
+
childSpecs.push(childInfo);
|
|
181
|
+
}
|
|
182
|
+
if (childSpecs.length > 0)
|
|
183
|
+
result.childSpecs = childSpecs;
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Resolve style node IDs to their actual visual values.
|
|
189
|
+
* Styles only contain metadata from the styles endpoint — we need getNodes to get actual colors/fonts/effects.
|
|
190
|
+
*/
|
|
191
|
+
async function resolveStyleValues(api, fileKey, styles) {
|
|
192
|
+
const resolved = new Map();
|
|
193
|
+
const nodeIds = styles.filter((s) => s.nodeId).map((s) => s.nodeId);
|
|
194
|
+
if (nodeIds.length === 0)
|
|
195
|
+
return resolved;
|
|
196
|
+
try {
|
|
197
|
+
const batchSize = 50;
|
|
198
|
+
for (let i = 0; i < nodeIds.length; i += batchSize) {
|
|
199
|
+
const batch = nodeIds.slice(i, i + batchSize);
|
|
200
|
+
const nodeResponse = await withTimeout(api.getNodes(fileKey, batch), 30000, `getStyleNodes(batch ${Math.floor(i / batchSize) + 1})`);
|
|
201
|
+
if (nodeResponse?.nodes) {
|
|
202
|
+
for (const [nodeId, nodeData] of Object.entries(nodeResponse.nodes)) {
|
|
203
|
+
const doc = nodeData?.document;
|
|
204
|
+
if (!doc)
|
|
205
|
+
continue;
|
|
206
|
+
const value = {};
|
|
207
|
+
// FILL styles → extract colors
|
|
208
|
+
if (doc.fills && Array.isArray(doc.fills)) {
|
|
209
|
+
value.fills = doc.fills
|
|
210
|
+
.filter((f) => f.visible !== false)
|
|
211
|
+
.map((f) => ({
|
|
212
|
+
type: f.type,
|
|
213
|
+
color: f.color ? rgbaToHex(f.color) : undefined,
|
|
214
|
+
opacity: f.opacity,
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
// TEXT styles → extract typography
|
|
218
|
+
if (doc.type === "TEXT" && doc.style) {
|
|
219
|
+
value.typography = {
|
|
220
|
+
fontFamily: doc.style.fontFamily,
|
|
221
|
+
fontSize: doc.style.fontSize,
|
|
222
|
+
fontWeight: doc.style.fontWeight,
|
|
223
|
+
lineHeight: doc.style.lineHeightPx,
|
|
224
|
+
letterSpacing: doc.style.letterSpacing,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// EFFECT styles → extract shadows/blurs
|
|
228
|
+
if (doc.effects && Array.isArray(doc.effects)) {
|
|
229
|
+
value.effects = doc.effects
|
|
230
|
+
.filter((e) => e.visible !== false)
|
|
231
|
+
.map((e) => ({
|
|
232
|
+
type: e.type,
|
|
233
|
+
color: e.color ? rgbaToHex(e.color) : undefined,
|
|
234
|
+
offset: e.offset,
|
|
235
|
+
radius: e.radius,
|
|
236
|
+
spread: e.spread,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
resolved.set(nodeId, value);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
logger.warn({ error: err }, "Failed to resolve style values");
|
|
246
|
+
}
|
|
247
|
+
return resolved;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Group variables by collection for a clean hierarchical output
|
|
251
|
+
*/
|
|
252
|
+
function groupVariablesByCollection(formatted) {
|
|
253
|
+
return formatted.collections.map((collection) => {
|
|
254
|
+
const collectionVars = formatted.variables
|
|
255
|
+
.filter((v) => v.variableCollectionId === collection.id)
|
|
256
|
+
.map((v) => ({
|
|
257
|
+
id: v.id,
|
|
258
|
+
name: v.name,
|
|
259
|
+
type: v.resolvedType,
|
|
260
|
+
description: v.description || undefined,
|
|
261
|
+
valuesByMode: v.valuesByMode,
|
|
262
|
+
scopes: v.scopes,
|
|
263
|
+
}));
|
|
264
|
+
return {
|
|
265
|
+
id: collection.id,
|
|
266
|
+
name: collection.name,
|
|
267
|
+
modes: collection.modes,
|
|
268
|
+
variables: collectionVars,
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Deduplicate components — filter out individual variants when their
|
|
274
|
+
* parent component set is already present.
|
|
275
|
+
*/
|
|
276
|
+
function deduplicateComponents(components, componentSets) {
|
|
277
|
+
const setNodeIds = new Set(componentSets.map((s) => s.node_id));
|
|
278
|
+
// Filter out variants that belong to a known component set
|
|
279
|
+
const standalone = components.filter((c) => {
|
|
280
|
+
if (c.containing_frame?.containingComponentSet) {
|
|
281
|
+
// This is a variant — check if parent set is already included
|
|
282
|
+
// Check both direct frame nodeId and the containingComponentSet.nodeId
|
|
283
|
+
// (some designs nest variants inside intermediate frames)
|
|
284
|
+
const frameNodeId = c.containing_frame?.nodeId;
|
|
285
|
+
const setNodeId = c.containing_frame?.containingComponentSet?.nodeId;
|
|
286
|
+
if ((frameNodeId && setNodeIds.has(frameNodeId)) ||
|
|
287
|
+
(setNodeId && setNodeIds.has(setNodeId))) {
|
|
288
|
+
return false; // Skip, parent set covers it
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
});
|
|
293
|
+
return { components: standalone, componentSets };
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Compress the kit for large responses
|
|
297
|
+
*/
|
|
298
|
+
function compressKit(kit, level) {
|
|
299
|
+
const compressed = { ...kit };
|
|
300
|
+
if (compressed.tokens) {
|
|
301
|
+
if (level === "compact") {
|
|
302
|
+
// Compact: only summary counts, drop all collections/variables
|
|
303
|
+
compressed.tokens = {
|
|
304
|
+
collections: [],
|
|
305
|
+
summary: compressed.tokens.summary,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
else if (level === "inventory") {
|
|
309
|
+
// Only keep variable names and types, drop values
|
|
310
|
+
compressed.tokens = {
|
|
311
|
+
...compressed.tokens,
|
|
312
|
+
collections: compressed.tokens.collections.map((c) => ({
|
|
313
|
+
...c,
|
|
314
|
+
variables: c.variables.map((v) => ({
|
|
315
|
+
id: v.id,
|
|
316
|
+
name: v.name,
|
|
317
|
+
type: v.type,
|
|
318
|
+
description: v.description,
|
|
319
|
+
valuesByMode: {}, // Strip values
|
|
320
|
+
scopes: v.scopes,
|
|
321
|
+
})),
|
|
322
|
+
})),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (compressed.components) {
|
|
327
|
+
if (level === "compact") {
|
|
328
|
+
// Compact: drastically reduce for large systems
|
|
329
|
+
// Separate component sets (design building blocks) from standalone components
|
|
330
|
+
const sets = compressed.components.items.filter((c) => c.variants && c.variants.length > 0);
|
|
331
|
+
const standalone = compressed.components.items.filter((c) => !c.variants || c.variants.length === 0);
|
|
332
|
+
// Keep all sets (they're the main building blocks), limit standalone to 100
|
|
333
|
+
const limitedStandalone = standalone.slice(0, 100);
|
|
334
|
+
const trimmedItems = [...sets, ...limitedStandalone];
|
|
335
|
+
compressed.components = {
|
|
336
|
+
...compressed.components,
|
|
337
|
+
items: trimmedItems.map((c) => ({
|
|
338
|
+
id: c.id,
|
|
339
|
+
name: c.name,
|
|
340
|
+
// Compact: variant count only (not individual names) for very large sets
|
|
341
|
+
variants: c.variants
|
|
342
|
+
? c.variants.length > 10
|
|
343
|
+
? [{ name: `${c.variants.length} variants`, id: "" }]
|
|
344
|
+
: c.variants.map((v) => ({ name: v.name, id: v.id }))
|
|
345
|
+
: undefined,
|
|
346
|
+
properties: c.properties
|
|
347
|
+
? Object.fromEntries(Object.entries(c.properties).map(([k, v]) => [
|
|
348
|
+
k,
|
|
349
|
+
{ type: v.type, defaultValue: v.defaultValue },
|
|
350
|
+
]))
|
|
351
|
+
: undefined,
|
|
352
|
+
})),
|
|
353
|
+
summary: {
|
|
354
|
+
...compressed.components.summary,
|
|
355
|
+
totalComponents: trimmedItems.length,
|
|
356
|
+
...(standalone.length > 100 ? { omittedStandaloneComponents: standalone.length - 100 } : {}),
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
else if (level === "inventory") {
|
|
361
|
+
// Only keep names and property keys — strip visual specs and variants
|
|
362
|
+
compressed.components = {
|
|
363
|
+
...compressed.components,
|
|
364
|
+
items: compressed.components.items.map((c) => ({
|
|
365
|
+
id: c.id,
|
|
366
|
+
name: c.name,
|
|
367
|
+
description: c.description,
|
|
368
|
+
properties: c.properties
|
|
369
|
+
? Object.fromEntries(Object.entries(c.properties).map(([k, v]) => [
|
|
370
|
+
k,
|
|
371
|
+
{ type: v.type, defaultValue: v.defaultValue },
|
|
372
|
+
]))
|
|
373
|
+
: undefined,
|
|
374
|
+
})),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
else if (level === "summary") {
|
|
378
|
+
// Keep visual specs but strip variant-level specs to save space
|
|
379
|
+
compressed.components = {
|
|
380
|
+
...compressed.components,
|
|
381
|
+
items: compressed.components.items.map((c) => ({
|
|
382
|
+
...c,
|
|
383
|
+
variants: c.variants?.map((v) => ({ name: v.name, id: v.id })),
|
|
384
|
+
})),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
// Drop image URLs at any compression level to save tokens
|
|
388
|
+
compressed.components.items = compressed.components.items.map((c) => {
|
|
389
|
+
const { imageUrl, ...rest } = c;
|
|
390
|
+
return rest;
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
if (compressed.styles) {
|
|
394
|
+
if (level === "compact") {
|
|
395
|
+
// Compact: only style names and types grouped by type, no resolved values
|
|
396
|
+
compressed.styles = {
|
|
397
|
+
...compressed.styles,
|
|
398
|
+
items: compressed.styles.items.map((s) => ({
|
|
399
|
+
key: s.key,
|
|
400
|
+
name: s.name,
|
|
401
|
+
styleType: s.styleType,
|
|
402
|
+
})),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
else if (level === "inventory") {
|
|
406
|
+
// Strip resolved values in inventory mode
|
|
407
|
+
compressed.styles = {
|
|
408
|
+
...compressed.styles,
|
|
409
|
+
items: compressed.styles.items.map((s) => ({
|
|
410
|
+
key: s.key,
|
|
411
|
+
name: s.name,
|
|
412
|
+
styleType: s.styleType,
|
|
413
|
+
description: s.description,
|
|
414
|
+
})),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return compressed;
|
|
419
|
+
}
|
|
420
|
+
// ============================================================================
|
|
421
|
+
// Tool Registration
|
|
422
|
+
// ============================================================================
|
|
423
|
+
export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, variablesCache, options) {
|
|
424
|
+
server.tool("figma_get_design_system_kit", "PREFERRED TOOL for design system extraction — replaces separate figma_get_styles, figma_get_variables, and figma_get_component calls. " +
|
|
425
|
+
"Returns tokens, components, and styles in a single optimized response with adaptive compression for large systems. " +
|
|
426
|
+
"Includes component visual specs (exact colors, padding, typography, layout), rendered screenshots, " +
|
|
427
|
+
"token values per mode (light/dark), and resolved style values. " +
|
|
428
|
+
"Use this instead of calling individual tools to avoid context window overflow. " +
|
|
429
|
+
"Ideal for AI code generation — use visualSpec for pixel-accurate reproduction.", {
|
|
430
|
+
fileKey: z
|
|
431
|
+
.string()
|
|
432
|
+
.optional()
|
|
433
|
+
.describe("Figma file key. If omitted, extracted from the current browser URL."),
|
|
434
|
+
include: z
|
|
435
|
+
.array(z.enum(["tokens", "components", "styles"]))
|
|
436
|
+
.optional()
|
|
437
|
+
.default(["tokens", "components", "styles"])
|
|
438
|
+
.describe("Which sections to include. Defaults to all."),
|
|
439
|
+
componentIds: z
|
|
440
|
+
.array(z.string())
|
|
441
|
+
.optional()
|
|
442
|
+
.describe("Optional list of specific component node IDs to include. If omitted, all published components are returned."),
|
|
443
|
+
includeImages: z
|
|
444
|
+
.boolean()
|
|
445
|
+
.optional()
|
|
446
|
+
.default(false)
|
|
447
|
+
.describe("Include image URLs for components (adds latency). Default false."),
|
|
448
|
+
format: z
|
|
449
|
+
.enum(["full", "summary", "compact"])
|
|
450
|
+
.optional()
|
|
451
|
+
.default("full")
|
|
452
|
+
.describe("'full' returns complete data with visual specs and resolved values. " +
|
|
453
|
+
"'summary' strips variant-level visual specs (medium payload). " +
|
|
454
|
+
"'compact' returns only names, types, and property definitions (smallest payload, best for large design systems). " +
|
|
455
|
+
"Auto-compresses if response exceeds safe size regardless of format setting."),
|
|
456
|
+
}, async ({ fileKey, include, componentIds, includeImages, format }) => {
|
|
457
|
+
try {
|
|
458
|
+
const api = await getFigmaAPI();
|
|
459
|
+
// Resolve file key
|
|
460
|
+
let resolvedFileKey = fileKey;
|
|
461
|
+
if (!resolvedFileKey) {
|
|
462
|
+
const currentUrl = getCurrentUrl();
|
|
463
|
+
if (currentUrl) {
|
|
464
|
+
resolvedFileKey = extractFileKey(currentUrl) || undefined;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (!resolvedFileKey) {
|
|
468
|
+
throw new Error("No file key provided and no Figma file currently open. " +
|
|
469
|
+
"Provide a fileKey parameter or navigate to a Figma file first.");
|
|
470
|
+
}
|
|
471
|
+
const errors = [];
|
|
472
|
+
const kit = {
|
|
473
|
+
fileKey: resolvedFileKey,
|
|
474
|
+
generatedAt: new Date().toISOString(),
|
|
475
|
+
format,
|
|
476
|
+
ai_instruction: "",
|
|
477
|
+
};
|
|
478
|
+
// ----------------------------------------------------------------
|
|
479
|
+
// Fetch tokens (variables)
|
|
480
|
+
// ----------------------------------------------------------------
|
|
481
|
+
if (include.includes("tokens")) {
|
|
482
|
+
try {
|
|
483
|
+
logger.info({ fileKey: resolvedFileKey }, "Fetching design tokens");
|
|
484
|
+
// Check cache first
|
|
485
|
+
let variablesData = null;
|
|
486
|
+
const cacheKey = `vars:${resolvedFileKey}`;
|
|
487
|
+
if (variablesCache) {
|
|
488
|
+
const cached = variablesCache.get(cacheKey);
|
|
489
|
+
if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
|
|
490
|
+
variablesData = cached.data;
|
|
491
|
+
logger.info("Using cached variables data");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (!variablesData) {
|
|
495
|
+
variablesData = await withTimeout(api.getLocalVariables(resolvedFileKey), 30000, "getLocalVariables");
|
|
496
|
+
if (variablesCache) {
|
|
497
|
+
variablesCache.set(cacheKey, {
|
|
498
|
+
data: variablesData,
|
|
499
|
+
timestamp: Date.now(),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const formatted = formatVariables(variablesData);
|
|
504
|
+
const collections = groupVariablesByCollection(formatted);
|
|
505
|
+
kit.tokens = {
|
|
506
|
+
collections,
|
|
507
|
+
summary: formatted.summary,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
512
|
+
logger.warn({ error: msg }, "Failed to fetch tokens");
|
|
513
|
+
errors.push({ section: "tokens", message: msg });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// ----------------------------------------------------------------
|
|
517
|
+
// Fetch components
|
|
518
|
+
// ----------------------------------------------------------------
|
|
519
|
+
if (include.includes("components")) {
|
|
520
|
+
try {
|
|
521
|
+
logger.info({ fileKey: resolvedFileKey }, "Fetching components");
|
|
522
|
+
const [componentsResponse, componentSetsResponse] = await Promise.all([
|
|
523
|
+
withTimeout(api.getComponents(resolvedFileKey), 30000, "getComponents"),
|
|
524
|
+
withTimeout(api.getComponentSets(resolvedFileKey), 30000, "getComponentSets"),
|
|
525
|
+
]);
|
|
526
|
+
const allComponents = componentsResponse?.meta?.components || [];
|
|
527
|
+
const allComponentSets = componentSetsResponse?.meta?.component_sets || [];
|
|
528
|
+
const { components: standaloneComponents, componentSets } = deduplicateComponents(allComponents, allComponentSets);
|
|
529
|
+
// Filter by component IDs if provided
|
|
530
|
+
let targetComponents = standaloneComponents;
|
|
531
|
+
let targetSets = componentSets;
|
|
532
|
+
if (componentIds && componentIds.length > 0) {
|
|
533
|
+
const idSet = new Set(componentIds);
|
|
534
|
+
targetComponents = standaloneComponents.filter((c) => idSet.has(c.node_id));
|
|
535
|
+
targetSets = componentSets.filter((s) => idSet.has(s.node_id));
|
|
536
|
+
}
|
|
537
|
+
// Build component specs
|
|
538
|
+
const componentSpecs = [];
|
|
539
|
+
// Collect all node IDs we need to fetch details for (batched, not N+1)
|
|
540
|
+
const allNodeIds = [
|
|
541
|
+
...targetSets.map((s) => s.node_id),
|
|
542
|
+
...targetComponents.map((c) => c.node_id),
|
|
543
|
+
];
|
|
544
|
+
// Batch fetch ALL node details in one call (max 50 per batch)
|
|
545
|
+
const nodeDetailsMap = {};
|
|
546
|
+
if (allNodeIds.length > 0) {
|
|
547
|
+
try {
|
|
548
|
+
const batchSize = 50;
|
|
549
|
+
for (let i = 0; i < allNodeIds.length; i += batchSize) {
|
|
550
|
+
const batch = allNodeIds.slice(i, i + batchSize);
|
|
551
|
+
const nodeResponse = await withTimeout(api.getNodes(resolvedFileKey, batch, { depth: 2 }), 30000, `getNodes(batch ${Math.floor(i / batchSize) + 1})`);
|
|
552
|
+
if (nodeResponse?.nodes) {
|
|
553
|
+
for (const [nodeId, nodeData] of Object.entries(nodeResponse.nodes)) {
|
|
554
|
+
nodeDetailsMap[nodeId] = nodeData?.document;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
logger.warn({ error: err }, "Failed to batch-fetch component node details");
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// Process component sets (multi-variant components)
|
|
564
|
+
for (const set of targetSets) {
|
|
565
|
+
const spec = {
|
|
566
|
+
id: set.node_id,
|
|
567
|
+
name: set.name,
|
|
568
|
+
description: set.description || undefined,
|
|
569
|
+
};
|
|
570
|
+
// Use pre-fetched node details
|
|
571
|
+
const setNode = nodeDetailsMap[set.node_id];
|
|
572
|
+
// Get variant info from the child components
|
|
573
|
+
// Match by component_set_id, containing_frame.nodeId, OR containingComponentSet.nodeId
|
|
574
|
+
// (some designs nest variants inside intermediate frames)
|
|
575
|
+
const variants = allComponents
|
|
576
|
+
.filter((c) => c.component_set_id === set.node_id ||
|
|
577
|
+
c.containing_frame?.nodeId === set.node_id ||
|
|
578
|
+
c.containing_frame?.containingComponentSet?.nodeId === set.node_id)
|
|
579
|
+
.map((c) => {
|
|
580
|
+
const entry = { name: c.name, id: c.node_id };
|
|
581
|
+
// Attach visual spec from depth-2 children of the set node
|
|
582
|
+
if (setNode?.children) {
|
|
583
|
+
const variantNode = setNode.children.find((ch) => ch.id === c.node_id);
|
|
584
|
+
if (variantNode) {
|
|
585
|
+
const vs = extractVisualSpec(variantNode);
|
|
586
|
+
if (vs)
|
|
587
|
+
entry.visualSpec = vs;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return entry;
|
|
591
|
+
});
|
|
592
|
+
if (variants.length > 0) {
|
|
593
|
+
spec.variants = variants;
|
|
594
|
+
}
|
|
595
|
+
if (setNode?.componentPropertyDefinitions) {
|
|
596
|
+
spec.properties = setNode.componentPropertyDefinitions;
|
|
597
|
+
}
|
|
598
|
+
if (setNode?.absoluteBoundingBox) {
|
|
599
|
+
spec.bounds = {
|
|
600
|
+
width: setNode.absoluteBoundingBox.width,
|
|
601
|
+
height: setNode.absoluteBoundingBox.height,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
// Extract visual spec from the set node (root + children)
|
|
605
|
+
if (setNode) {
|
|
606
|
+
const visualData = extractComponentVisualData(setNode);
|
|
607
|
+
if (visualData.visualSpec) {
|
|
608
|
+
spec.visualSpec = visualData.visualSpec;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
componentSpecs.push(spec);
|
|
612
|
+
}
|
|
613
|
+
// Process standalone components (not part of a set)
|
|
614
|
+
for (const comp of targetComponents) {
|
|
615
|
+
const spec = {
|
|
616
|
+
id: comp.node_id,
|
|
617
|
+
name: comp.name,
|
|
618
|
+
description: comp.description || undefined,
|
|
619
|
+
};
|
|
620
|
+
// Use pre-fetched node details
|
|
621
|
+
const node = nodeDetailsMap[comp.node_id];
|
|
622
|
+
if (node?.componentPropertyDefinitions) {
|
|
623
|
+
spec.properties = node.componentPropertyDefinitions;
|
|
624
|
+
}
|
|
625
|
+
if (node?.absoluteBoundingBox) {
|
|
626
|
+
spec.bounds = {
|
|
627
|
+
width: node.absoluteBoundingBox.width,
|
|
628
|
+
height: node.absoluteBoundingBox.height,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
// Extract visual spec from the component node (root + children)
|
|
632
|
+
if (node) {
|
|
633
|
+
const visualData = extractComponentVisualData(node);
|
|
634
|
+
if (visualData.visualSpec) {
|
|
635
|
+
spec.visualSpec = visualData.visualSpec;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
componentSpecs.push(spec);
|
|
639
|
+
}
|
|
640
|
+
// Optionally fetch component images
|
|
641
|
+
if (includeImages && componentSpecs.length > 0) {
|
|
642
|
+
try {
|
|
643
|
+
const nodeIds = componentSpecs.map((c) => c.id);
|
|
644
|
+
// Batch in groups of 50 to stay within API limits
|
|
645
|
+
const batchSize = 50;
|
|
646
|
+
for (let i = 0; i < nodeIds.length; i += batchSize) {
|
|
647
|
+
const batch = nodeIds.slice(i, i + batchSize);
|
|
648
|
+
const imagesResult = await withTimeout(api.getImages(resolvedFileKey, batch, { scale: 2, format: "png" }), 30000, "getImages");
|
|
649
|
+
if (imagesResult?.images) {
|
|
650
|
+
for (const spec of componentSpecs) {
|
|
651
|
+
const url = imagesResult.images[spec.id];
|
|
652
|
+
if (url) {
|
|
653
|
+
spec.imageUrl = url;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
661
|
+
logger.warn({ error: msg }, "Failed to fetch component images");
|
|
662
|
+
errors.push({ section: "component_images", message: msg });
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
kit.components = {
|
|
666
|
+
items: componentSpecs,
|
|
667
|
+
summary: {
|
|
668
|
+
totalComponents: componentSpecs.length,
|
|
669
|
+
totalComponentSets: targetSets.length,
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
675
|
+
logger.warn({ error: msg }, "Failed to fetch components");
|
|
676
|
+
errors.push({ section: "components", message: msg });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// ----------------------------------------------------------------
|
|
680
|
+
// Fetch styles
|
|
681
|
+
// ----------------------------------------------------------------
|
|
682
|
+
if (include.includes("styles")) {
|
|
683
|
+
try {
|
|
684
|
+
logger.info({ fileKey: resolvedFileKey }, "Fetching styles");
|
|
685
|
+
const stylesResponse = await withTimeout(api.getStyles(resolvedFileKey), 30000, "getStyles");
|
|
686
|
+
const allStyles = stylesResponse?.meta?.styles || [];
|
|
687
|
+
const styleSpecs = allStyles.map((s) => ({
|
|
688
|
+
key: s.key,
|
|
689
|
+
name: s.name,
|
|
690
|
+
styleType: s.style_type,
|
|
691
|
+
description: s.description || undefined,
|
|
692
|
+
nodeId: s.node_id,
|
|
693
|
+
}));
|
|
694
|
+
// Resolve actual values for styles (colors, typography, effects)
|
|
695
|
+
if (styleSpecs.length > 0) {
|
|
696
|
+
try {
|
|
697
|
+
const resolvedValues = await resolveStyleValues(api, resolvedFileKey, styleSpecs);
|
|
698
|
+
for (const style of styleSpecs) {
|
|
699
|
+
if (style.nodeId && resolvedValues.has(style.nodeId)) {
|
|
700
|
+
style.resolvedValue = resolvedValues.get(style.nodeId);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
catch (err) {
|
|
705
|
+
logger.warn({ error: err }, "Failed to resolve style values");
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const stylesByType = {};
|
|
709
|
+
for (const s of styleSpecs) {
|
|
710
|
+
stylesByType[s.styleType] = (stylesByType[s.styleType] || 0) + 1;
|
|
711
|
+
}
|
|
712
|
+
kit.styles = {
|
|
713
|
+
items: styleSpecs,
|
|
714
|
+
summary: {
|
|
715
|
+
totalStyles: styleSpecs.length,
|
|
716
|
+
stylesByType,
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
catch (err) {
|
|
721
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
722
|
+
logger.warn({ error: msg }, "Failed to fetch styles");
|
|
723
|
+
errors.push({ section: "styles", message: msg });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// ----------------------------------------------------------------
|
|
727
|
+
// Build AI instruction
|
|
728
|
+
// ----------------------------------------------------------------
|
|
729
|
+
if (errors.length > 0) {
|
|
730
|
+
kit.errors = errors;
|
|
731
|
+
}
|
|
732
|
+
const sections = [];
|
|
733
|
+
if (kit.tokens)
|
|
734
|
+
sections.push(`${kit.tokens.summary.totalVariables} tokens in ${kit.tokens.summary.totalCollections} collections`);
|
|
735
|
+
if (kit.components)
|
|
736
|
+
sections.push(`${kit.components.summary.totalComponents} components (${kit.components.summary.totalComponentSets} sets)`);
|
|
737
|
+
if (kit.styles)
|
|
738
|
+
sections.push(`${kit.styles.summary.totalStyles} styles`);
|
|
739
|
+
kit.ai_instruction =
|
|
740
|
+
"DESIGN SYSTEM SPECIFICATION — STRICT VISUAL FIDELITY REQUIRED\n\n" +
|
|
741
|
+
`Contains: ${sections.join(", ")}.\n\n` +
|
|
742
|
+
"RULES:\n" +
|
|
743
|
+
"1. ONLY use colors, spacing, and typography values from this data. " +
|
|
744
|
+
"Do NOT invent, guess, or add any visual properties not explicitly present.\n" +
|
|
745
|
+
"2. Map 'visualSpec' directly to CSS:\n" +
|
|
746
|
+
" - fills[].color → background-color (e.g. #181818)\n" +
|
|
747
|
+
" - strokes[].color/weight → border (e.g. 1px solid #9747FF)\n" +
|
|
748
|
+
" - effects[] → box-shadow (type DROP_SHADOW: offset.x offset.y radius spread color)\n" +
|
|
749
|
+
" - cornerRadius → border-radius\n" +
|
|
750
|
+
" - layout.mode HORIZONTAL → flex-direction:row, VERTICAL → flex-direction:column\n" +
|
|
751
|
+
" - layout.paddingTop/Right/Bottom/Left → padding\n" +
|
|
752
|
+
" - layout.itemSpacing → gap\n" +
|
|
753
|
+
" - layout.primaryAxisAlign → justify-content, counterAxisAlign → align-items\n" +
|
|
754
|
+
" - typography → font-family, font-size, font-weight, line-height, letter-spacing\n" +
|
|
755
|
+
"3. Do NOT add decorative elements (colored borders, accents, dividers, gradients) " +
|
|
756
|
+
"unless they appear in the visualSpec data.\n" +
|
|
757
|
+
"4. Use 'imageUrl' screenshots as the visual ground truth. If the screenshot " +
|
|
758
|
+
"shows a simple dark card, do not add colored side borders or other embellishments.\n" +
|
|
759
|
+
"5. Style 'resolvedValue' contains the exact design system colors and typography — " +
|
|
760
|
+
"match these values precisely, do not substitute similar colors.\n" +
|
|
761
|
+
"6. Component 'properties' define the component API (props). " +
|
|
762
|
+
"VARIANT type properties define the visual variants (e.g. Info, Danger, Success). " +
|
|
763
|
+
"BOOLEAN properties toggle features. TEXT properties accept string content.\n" +
|
|
764
|
+
"7. When applying to an existing component library (e.g. shadcn, MUI, Chakra), " +
|
|
765
|
+
"override the library's default theme values with the exact colors, spacing, and " +
|
|
766
|
+
"typography from this specification. Do not blend with library defaults.";
|
|
767
|
+
// ----------------------------------------------------------------
|
|
768
|
+
// Adaptive compression for large responses
|
|
769
|
+
// Thresholds tuned for consumer AI context windows (~128K tokens ≈ ~400KB text)
|
|
770
|
+
// ----------------------------------------------------------------
|
|
771
|
+
const sizeKB = calculateSizeKB(kit);
|
|
772
|
+
logger.info({ sizeKB: sizeKB.toFixed(0), format }, "Kit assembled, checking compression");
|
|
773
|
+
// Determine compression level from format + size
|
|
774
|
+
let compressionLevel = null;
|
|
775
|
+
if (format === "compact") {
|
|
776
|
+
compressionLevel = "compact";
|
|
777
|
+
}
|
|
778
|
+
else if (format === "summary") {
|
|
779
|
+
compressionLevel = "summary";
|
|
780
|
+
}
|
|
781
|
+
// Auto-compress based on size regardless of format setting
|
|
782
|
+
// Lower thresholds to stay within consumer context windows
|
|
783
|
+
if (sizeKB > 500) {
|
|
784
|
+
compressionLevel = "compact"; // >500KB → just names and types
|
|
785
|
+
}
|
|
786
|
+
else if (sizeKB > 200) {
|
|
787
|
+
// Upgrade to at least inventory if not already more aggressive
|
|
788
|
+
if (!compressionLevel || compressionLevel === "summary") {
|
|
789
|
+
compressionLevel = "inventory";
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
else if (sizeKB > 100) {
|
|
793
|
+
// Upgrade to at least summary
|
|
794
|
+
if (!compressionLevel) {
|
|
795
|
+
compressionLevel = "summary";
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (compressionLevel) {
|
|
799
|
+
const compressed = compressKit(kit, compressionLevel);
|
|
800
|
+
const compressedSizeKB = calculateSizeKB(compressed);
|
|
801
|
+
if (sizeKB > 100) {
|
|
802
|
+
compressed.ai_instruction =
|
|
803
|
+
`Response auto-compressed (${compressionLevel}) from ${sizeKB.toFixed(0)}KB to ${compressedSizeKB.toFixed(0)}KB. ` +
|
|
804
|
+
compressed.ai_instruction +
|
|
805
|
+
" For full visual specs of specific components, re-call with specific componentIds and format='full'.";
|
|
806
|
+
}
|
|
807
|
+
logger.info({ originalKB: sizeKB.toFixed(0), compressedKB: compressedSizeKB.toFixed(0), level: compressionLevel }, "Kit compressed");
|
|
808
|
+
return {
|
|
809
|
+
content: [
|
|
810
|
+
{
|
|
811
|
+
type: "text",
|
|
812
|
+
text: JSON.stringify(compressed),
|
|
813
|
+
},
|
|
814
|
+
],
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
content: [
|
|
819
|
+
{
|
|
820
|
+
type: "text",
|
|
821
|
+
text: JSON.stringify(kit),
|
|
822
|
+
},
|
|
823
|
+
],
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
logger.error({ error }, "Failed to generate design system kit");
|
|
828
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
829
|
+
// Check if it's an auth error
|
|
830
|
+
let parsedError = null;
|
|
831
|
+
try {
|
|
832
|
+
parsedError = JSON.parse(errorMessage);
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
// Not a JSON error
|
|
836
|
+
}
|
|
837
|
+
if (parsedError?.error === "authentication_required" || parsedError?.error === "oauth_error") {
|
|
838
|
+
return {
|
|
839
|
+
content: [
|
|
840
|
+
{
|
|
841
|
+
type: "text",
|
|
842
|
+
text: JSON.stringify(parsedError),
|
|
843
|
+
},
|
|
844
|
+
],
|
|
845
|
+
isError: true,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
return {
|
|
849
|
+
content: [
|
|
850
|
+
{
|
|
851
|
+
type: "text",
|
|
852
|
+
text: JSON.stringify({
|
|
853
|
+
error: errorMessage,
|
|
854
|
+
message: "Failed to generate design system kit",
|
|
855
|
+
hint: "Ensure you have a valid Figma file key and the file contains published components/variables.",
|
|
856
|
+
}),
|
|
857
|
+
},
|
|
858
|
+
],
|
|
859
|
+
isError: true,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
}
|