@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,2091 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createChildLogger } from "./logger.js";
|
|
3
|
+
const logger = createChildLogger({ component: "write-tools" });
|
|
4
|
+
/**
|
|
5
|
+
* Register write/manipulation tools that require a Desktop Bridge connector.
|
|
6
|
+
* Used by both local mode (src/local.ts) and cloud mode (src/index.ts).
|
|
7
|
+
*/
|
|
8
|
+
export function registerWriteTools(server, getDesktopConnector) {
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// EXECUTION TOOL
|
|
11
|
+
// ============================================================================
|
|
12
|
+
server.tool("figma_execute", `Execute arbitrary JavaScript in Figma's plugin context with full access to the figma API. Use for complex operations not covered by other tools. Requires Desktop Bridge plugin. CAUTION: Can modify your document.
|
|
13
|
+
|
|
14
|
+
**COMPONENT INSTANCES:** For instances (node.type === 'INSTANCE'), use figma_set_instance_properties — direct text editing FAILS SILENTLY. Check instance.componentProperties for available props (may have #nodeId suffixes).
|
|
15
|
+
|
|
16
|
+
**RESULT ANALYSIS:** Check resultAnalysis.warning for silent failures (empty arrays, null returns).
|
|
17
|
+
|
|
18
|
+
**VALIDATION:** After creating/modifying visuals: screenshot with figma_capture_screenshot, check alignment/spacing/proportions, iterate up to 3x.
|
|
19
|
+
|
|
20
|
+
**PLACEMENT:** Always create components inside a Section or Frame, never on blank canvas. Use parent.insertChild(0, bg) for z-ordering backgrounds behind content.`, {
|
|
21
|
+
code: z
|
|
22
|
+
.string()
|
|
23
|
+
.describe("JavaScript code to execute. Has access to the 'figma' global object. " +
|
|
24
|
+
"Example: 'const rect = figma.createRectangle(); rect.resize(100, 100); return { id: rect.id };'"),
|
|
25
|
+
timeout: z
|
|
26
|
+
.number()
|
|
27
|
+
.optional()
|
|
28
|
+
.default(5000)
|
|
29
|
+
.describe("Execution timeout in milliseconds (default: 5000, max: 30000)"),
|
|
30
|
+
}, async ({ code, timeout }) => {
|
|
31
|
+
try {
|
|
32
|
+
const connector = await getDesktopConnector();
|
|
33
|
+
const result = await connector.executeCodeViaUI(code, Math.min(timeout, 30000));
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: JSON.stringify({
|
|
39
|
+
success: result.success,
|
|
40
|
+
result: result.result,
|
|
41
|
+
error: result.error,
|
|
42
|
+
resultAnalysis: result.resultAnalysis,
|
|
43
|
+
fileContext: result.fileContext,
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
logger.error({ error }, "Failed to execute code in Figma plugin context");
|
|
52
|
+
return {
|
|
53
|
+
content: [
|
|
54
|
+
{
|
|
55
|
+
type: "text",
|
|
56
|
+
text: JSON.stringify({
|
|
57
|
+
error: error instanceof Error ? error.message : String(error),
|
|
58
|
+
message: "Failed to execute code in Figma plugin context",
|
|
59
|
+
hint: "Make sure the Desktop Bridge plugin is running in Figma",
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
isError: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// VARIABLE MANAGEMENT TOOLS
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Tool: Update a variable's value
|
|
71
|
+
server.tool("figma_update_variable", "Update a single variable's value. For multiple updates, use figma_batch_update_variables instead (10-50x faster). Use figma_get_variables first for IDs. COLOR: hex '#FF0000', FLOAT: number, STRING: text, BOOLEAN: true/false. Requires Desktop Bridge plugin.", {
|
|
72
|
+
variableId: z
|
|
73
|
+
.string()
|
|
74
|
+
.describe("The variable ID to update (e.g., 'VariableID:123:456'). Get this from figma_get_variables."),
|
|
75
|
+
modeId: z
|
|
76
|
+
.string()
|
|
77
|
+
.describe("The mode ID to update the value in (e.g., '1:0'). Get this from the variable's collection modes."),
|
|
78
|
+
value: z
|
|
79
|
+
.union([z.string(), z.number(), z.boolean()])
|
|
80
|
+
.describe("The new value. For COLOR: hex string like '#FF0000'. For FLOAT: number. For STRING: text. For BOOLEAN: true/false."),
|
|
81
|
+
}, async ({ variableId, modeId, value }) => {
|
|
82
|
+
try {
|
|
83
|
+
const connector = await getDesktopConnector();
|
|
84
|
+
const result = await connector.updateVariable(variableId, modeId, value);
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: JSON.stringify({
|
|
90
|
+
success: true,
|
|
91
|
+
message: `Variable "${result.variable.name}" updated successfully`,
|
|
92
|
+
variable: result.variable,
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger.error({ error }, "Failed to update variable");
|
|
101
|
+
return {
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: JSON.stringify({
|
|
106
|
+
error: error instanceof Error ? error.message : String(error),
|
|
107
|
+
message: "Failed to update variable",
|
|
108
|
+
hint: "Make sure the Desktop Bridge plugin is running and the variable ID is correct",
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
isError: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
// Tool: Create a new variable
|
|
117
|
+
server.tool("figma_create_variable", "Create a single Figma variable. For multiple variables, use figma_batch_create_variables instead (10-50x faster). Use figma_get_variables first to get collection IDs. Supports COLOR, FLOAT, STRING, BOOLEAN. Requires Desktop Bridge plugin.", {
|
|
118
|
+
name: z
|
|
119
|
+
.string()
|
|
120
|
+
.describe("Name for the new variable (e.g., 'primary-blue')"),
|
|
121
|
+
collectionId: z
|
|
122
|
+
.string()
|
|
123
|
+
.describe("The collection ID to create the variable in (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
|
|
124
|
+
resolvedType: z
|
|
125
|
+
.enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"])
|
|
126
|
+
.describe("The variable type: COLOR, FLOAT, STRING, or BOOLEAN"),
|
|
127
|
+
description: z
|
|
128
|
+
.string()
|
|
129
|
+
.optional()
|
|
130
|
+
.describe("Optional description for the variable"),
|
|
131
|
+
valuesByMode: z
|
|
132
|
+
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
|
133
|
+
.optional()
|
|
134
|
+
.describe("Optional initial values by mode ID. Example: { '1:0': '#FF0000', '1:1': '#0000FF' }"),
|
|
135
|
+
}, async ({ name, collectionId, resolvedType, description, valuesByMode, }) => {
|
|
136
|
+
try {
|
|
137
|
+
const connector = await getDesktopConnector();
|
|
138
|
+
const result = await connector.createVariable(name, collectionId, resolvedType, {
|
|
139
|
+
description,
|
|
140
|
+
valuesByMode,
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: "text",
|
|
146
|
+
text: JSON.stringify({
|
|
147
|
+
success: true,
|
|
148
|
+
message: `Variable "${name}" created successfully`,
|
|
149
|
+
variable: result.variable,
|
|
150
|
+
timestamp: Date.now(),
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
logger.error({ error }, "Failed to create variable");
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: "text",
|
|
162
|
+
text: JSON.stringify({
|
|
163
|
+
error: error instanceof Error ? error.message : String(error),
|
|
164
|
+
message: "Failed to create variable",
|
|
165
|
+
hint: "Make sure the Desktop Bridge plugin is running and the collection ID is correct",
|
|
166
|
+
}),
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
isError: true,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
// Tool: Create a new variable collection
|
|
174
|
+
server.tool("figma_create_variable_collection", "Create an empty variable collection. To create a collection WITH variables and modes in one step, use figma_setup_design_tokens instead. Requires Desktop Bridge plugin.", {
|
|
175
|
+
name: z
|
|
176
|
+
.string()
|
|
177
|
+
.describe("Name for the new collection (e.g., 'Brand Colors')"),
|
|
178
|
+
initialModeName: z
|
|
179
|
+
.string()
|
|
180
|
+
.optional()
|
|
181
|
+
.describe("Name for the initial mode (default mode is created automatically). Example: 'Light'"),
|
|
182
|
+
additionalModes: z
|
|
183
|
+
.array(z.string())
|
|
184
|
+
.optional()
|
|
185
|
+
.describe("Additional mode names to create. Example: ['Dark', 'High Contrast']"),
|
|
186
|
+
}, async ({ name, initialModeName, additionalModes }) => {
|
|
187
|
+
try {
|
|
188
|
+
const connector = await getDesktopConnector();
|
|
189
|
+
const result = await connector.createVariableCollection(name, {
|
|
190
|
+
initialModeName,
|
|
191
|
+
additionalModes,
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: "text",
|
|
197
|
+
text: JSON.stringify({
|
|
198
|
+
success: true,
|
|
199
|
+
message: `Collection "${name}" created successfully`,
|
|
200
|
+
collection: result.collection,
|
|
201
|
+
timestamp: Date.now(),
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
logger.error({ error }, "Failed to create collection");
|
|
209
|
+
return {
|
|
210
|
+
content: [
|
|
211
|
+
{
|
|
212
|
+
type: "text",
|
|
213
|
+
text: JSON.stringify({
|
|
214
|
+
error: error instanceof Error ? error.message : String(error),
|
|
215
|
+
message: "Failed to create variable collection",
|
|
216
|
+
hint: "Make sure the Desktop Bridge plugin is running in Figma",
|
|
217
|
+
}),
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
isError: true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
// Tool: Delete a variable
|
|
225
|
+
server.tool("figma_delete_variable", "Delete a Figma variable. WARNING: This is a destructive operation that cannot be undone (except with Figma's undo). Use figma_get_variables first to get variable IDs. Requires the Desktop Bridge plugin to be running.", {
|
|
226
|
+
variableId: z
|
|
227
|
+
.string()
|
|
228
|
+
.describe("The variable ID to delete (e.g., 'VariableID:123:456'). Get this from figma_get_variables."),
|
|
229
|
+
}, async ({ variableId }) => {
|
|
230
|
+
try {
|
|
231
|
+
const connector = await getDesktopConnector();
|
|
232
|
+
const result = await connector.deleteVariable(variableId);
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: JSON.stringify({
|
|
238
|
+
success: true,
|
|
239
|
+
message: `Variable "${result.deleted.name}" deleted successfully`,
|
|
240
|
+
deleted: result.deleted,
|
|
241
|
+
timestamp: Date.now(),
|
|
242
|
+
warning: "This action cannot be undone programmatically. Use Figma's Edit > Undo if needed.",
|
|
243
|
+
}),
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
logger.error({ error }, "Failed to delete variable");
|
|
250
|
+
return {
|
|
251
|
+
content: [
|
|
252
|
+
{
|
|
253
|
+
type: "text",
|
|
254
|
+
text: JSON.stringify({
|
|
255
|
+
error: error instanceof Error ? error.message : String(error),
|
|
256
|
+
message: "Failed to delete variable",
|
|
257
|
+
hint: "Make sure the Desktop Bridge plugin is running and the variable ID is correct",
|
|
258
|
+
}),
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
isError: true,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
// Tool: Delete a variable collection
|
|
266
|
+
server.tool("figma_delete_variable_collection", "Delete a Figma variable collection and ALL its variables. WARNING: This is a destructive operation that deletes all variables in the collection and cannot be undone (except with Figma's undo). Requires the Desktop Bridge plugin to be running.", {
|
|
267
|
+
collectionId: z
|
|
268
|
+
.string()
|
|
269
|
+
.describe("The collection ID to delete (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
|
|
270
|
+
}, async ({ collectionId }) => {
|
|
271
|
+
try {
|
|
272
|
+
const connector = await getDesktopConnector();
|
|
273
|
+
const result = await connector.deleteVariableCollection(collectionId);
|
|
274
|
+
return {
|
|
275
|
+
content: [
|
|
276
|
+
{
|
|
277
|
+
type: "text",
|
|
278
|
+
text: JSON.stringify({
|
|
279
|
+
success: true,
|
|
280
|
+
message: `Collection "${result.deleted.name}" and ${result.deleted.variableCount} variables deleted successfully`,
|
|
281
|
+
deleted: result.deleted,
|
|
282
|
+
timestamp: Date.now(),
|
|
283
|
+
warning: "This action cannot be undone programmatically. Use Figma's Edit > Undo if needed.",
|
|
284
|
+
}),
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
logger.error({ error }, "Failed to delete collection");
|
|
291
|
+
return {
|
|
292
|
+
content: [
|
|
293
|
+
{
|
|
294
|
+
type: "text",
|
|
295
|
+
text: JSON.stringify({
|
|
296
|
+
error: error instanceof Error ? error.message : String(error),
|
|
297
|
+
message: "Failed to delete variable collection",
|
|
298
|
+
hint: "Make sure the Desktop Bridge plugin is running and the collection ID is correct",
|
|
299
|
+
}),
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
isError: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
// Tool: Rename a variable
|
|
307
|
+
server.tool("figma_rename_variable", "Rename an existing Figma variable. This updates the variable's name while preserving all its values and settings. Requires the Desktop Bridge plugin to be running.", {
|
|
308
|
+
variableId: z
|
|
309
|
+
.string()
|
|
310
|
+
.describe("The variable ID to rename (e.g., 'VariableID:123:456'). Get this from figma_get_variables."),
|
|
311
|
+
newName: z
|
|
312
|
+
.string()
|
|
313
|
+
.describe("The new name for the variable. Can include slashes for grouping (e.g., 'colors/primary/background')."),
|
|
314
|
+
}, async ({ variableId, newName }) => {
|
|
315
|
+
try {
|
|
316
|
+
const connector = await getDesktopConnector();
|
|
317
|
+
const result = await connector.renameVariable(variableId, newName);
|
|
318
|
+
return {
|
|
319
|
+
content: [
|
|
320
|
+
{
|
|
321
|
+
type: "text",
|
|
322
|
+
text: JSON.stringify({
|
|
323
|
+
success: true,
|
|
324
|
+
message: `Variable renamed from "${result.oldName}" to "${result.variable.name}"`,
|
|
325
|
+
oldName: result.oldName,
|
|
326
|
+
variable: result.variable,
|
|
327
|
+
timestamp: Date.now(),
|
|
328
|
+
}),
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
logger.error({ error }, "Failed to rename variable");
|
|
335
|
+
return {
|
|
336
|
+
content: [
|
|
337
|
+
{
|
|
338
|
+
type: "text",
|
|
339
|
+
text: JSON.stringify({
|
|
340
|
+
error: error instanceof Error ? error.message : String(error),
|
|
341
|
+
message: "Failed to rename variable",
|
|
342
|
+
hint: "Make sure the Desktop Bridge plugin is running and the variable ID is correct",
|
|
343
|
+
}),
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
isError: true,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
// Tool: Add a mode to a collection
|
|
351
|
+
server.tool("figma_add_mode", "Add a new mode to an existing Figma variable collection. Modes allow variables to have different values for different contexts (e.g., Light/Dark themes, device sizes). Requires the Desktop Bridge plugin to be running.", {
|
|
352
|
+
collectionId: z
|
|
353
|
+
.string()
|
|
354
|
+
.describe("The collection ID to add the mode to (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
|
|
355
|
+
modeName: z
|
|
356
|
+
.string()
|
|
357
|
+
.describe("The name for the new mode (e.g., 'Dark', 'Mobile', 'High Contrast')."),
|
|
358
|
+
}, async ({ collectionId, modeName }) => {
|
|
359
|
+
try {
|
|
360
|
+
const connector = await getDesktopConnector();
|
|
361
|
+
const result = await connector.addMode(collectionId, modeName);
|
|
362
|
+
return {
|
|
363
|
+
content: [
|
|
364
|
+
{
|
|
365
|
+
type: "text",
|
|
366
|
+
text: JSON.stringify({
|
|
367
|
+
success: true,
|
|
368
|
+
message: `Mode "${modeName}" added to collection "${result.collection.name}"`,
|
|
369
|
+
newMode: result.newMode,
|
|
370
|
+
collection: result.collection,
|
|
371
|
+
timestamp: Date.now(),
|
|
372
|
+
}),
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
logger.error({ error }, "Failed to add mode");
|
|
379
|
+
return {
|
|
380
|
+
content: [
|
|
381
|
+
{
|
|
382
|
+
type: "text",
|
|
383
|
+
text: JSON.stringify({
|
|
384
|
+
error: error instanceof Error ? error.message : String(error),
|
|
385
|
+
message: "Failed to add mode to collection",
|
|
386
|
+
hint: "Make sure the Desktop Bridge plugin is running, the collection ID is correct, and you haven't exceeded Figma's mode limit",
|
|
387
|
+
}),
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
isError: true,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
// Tool: Rename a mode in a collection
|
|
395
|
+
server.tool("figma_rename_mode", "Rename an existing mode in a Figma variable collection. Requires the Desktop Bridge plugin to be running.", {
|
|
396
|
+
collectionId: z
|
|
397
|
+
.string()
|
|
398
|
+
.describe("The collection ID containing the mode (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
|
|
399
|
+
modeId: z
|
|
400
|
+
.string()
|
|
401
|
+
.describe("The mode ID to rename (e.g., '123:0'). Get this from the collection's modes array in figma_get_variables."),
|
|
402
|
+
newName: z
|
|
403
|
+
.string()
|
|
404
|
+
.describe("The new name for the mode (e.g., 'Dark Theme', 'Tablet')."),
|
|
405
|
+
}, async ({ collectionId, modeId, newName }) => {
|
|
406
|
+
try {
|
|
407
|
+
const connector = await getDesktopConnector();
|
|
408
|
+
const result = await connector.renameMode(collectionId, modeId, newName);
|
|
409
|
+
return {
|
|
410
|
+
content: [
|
|
411
|
+
{
|
|
412
|
+
type: "text",
|
|
413
|
+
text: JSON.stringify({
|
|
414
|
+
success: true,
|
|
415
|
+
message: `Mode renamed from "${result.oldName}" to "${newName}"`,
|
|
416
|
+
oldName: result.oldName,
|
|
417
|
+
collection: result.collection,
|
|
418
|
+
timestamp: Date.now(),
|
|
419
|
+
}),
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
logger.error({ error }, "Failed to rename mode");
|
|
426
|
+
return {
|
|
427
|
+
content: [
|
|
428
|
+
{
|
|
429
|
+
type: "text",
|
|
430
|
+
text: JSON.stringify({
|
|
431
|
+
error: error instanceof Error ? error.message : String(error),
|
|
432
|
+
message: "Failed to rename mode",
|
|
433
|
+
hint: "Make sure the Desktop Bridge plugin is running, the collection ID and mode ID are correct",
|
|
434
|
+
}),
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
isError: true,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
// ============================================================================
|
|
442
|
+
// BATCH OPERATIONS (Performance-Optimized)
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// Execute multiple variable operations in a single roundtrip,
|
|
445
|
+
// reducing per-operation overhead from ~60-170ms to near-zero.
|
|
446
|
+
// Use these instead of calling individual tools repeatedly.
|
|
447
|
+
// Tool: Batch create variables
|
|
448
|
+
server.tool("figma_batch_create_variables", "Create multiple variables in one operation. Use instead of calling figma_create_variable repeatedly — up to 50x faster for bulk operations. Get collection IDs from figma_get_variables first. Requires Desktop Bridge plugin.", {
|
|
449
|
+
collectionId: z
|
|
450
|
+
.string()
|
|
451
|
+
.describe("Collection ID to create all variables in (e.g., 'VariableCollectionId:123:456')"),
|
|
452
|
+
variables: z
|
|
453
|
+
.array(z.object({
|
|
454
|
+
name: z.string().describe("Variable name (e.g., 'primary-blue')"),
|
|
455
|
+
resolvedType: z
|
|
456
|
+
.enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"])
|
|
457
|
+
.describe("Variable type"),
|
|
458
|
+
description: z
|
|
459
|
+
.string()
|
|
460
|
+
.optional()
|
|
461
|
+
.describe("Optional description"),
|
|
462
|
+
valuesByMode: z
|
|
463
|
+
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
|
464
|
+
.optional()
|
|
465
|
+
.describe("Values by mode ID. For COLOR: hex like '#FF0000'. Example: { '1:0': '#FF0000' }"),
|
|
466
|
+
}))
|
|
467
|
+
.min(1)
|
|
468
|
+
.max(100)
|
|
469
|
+
.describe("Array of variables to create (1-100)"),
|
|
470
|
+
}, async ({ collectionId, variables }) => {
|
|
471
|
+
try {
|
|
472
|
+
const connector = await getDesktopConnector();
|
|
473
|
+
const script = `
|
|
474
|
+
const results = [];
|
|
475
|
+
const collectionId = ${JSON.stringify(collectionId)};
|
|
476
|
+
const vars = ${JSON.stringify(variables)};
|
|
477
|
+
|
|
478
|
+
function hexToRgba(hex) {
|
|
479
|
+
hex = hex.replace('#', '');
|
|
480
|
+
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
|
|
481
|
+
return {
|
|
482
|
+
r: parseInt(hex.substring(0, 2), 16) / 255,
|
|
483
|
+
g: parseInt(hex.substring(2, 4), 16) / 255,
|
|
484
|
+
b: parseInt(hex.substring(4, 6), 16) / 255,
|
|
485
|
+
a: hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const collection = await figma.variables.getVariableCollectionByIdAsync(collectionId);
|
|
490
|
+
if (!collection) return { created: 0, failed: vars.length, results: vars.map(v => ({ success: false, name: v.name, error: 'Collection not found: ' + collectionId })) };
|
|
491
|
+
|
|
492
|
+
for (const v of vars) {
|
|
493
|
+
try {
|
|
494
|
+
const variable = figma.variables.createVariable(v.name, collection, v.resolvedType);
|
|
495
|
+
if (v.description) variable.description = v.description;
|
|
496
|
+
if (v.valuesByMode) {
|
|
497
|
+
for (const [modeId, value] of Object.entries(v.valuesByMode)) {
|
|
498
|
+
const processed = v.resolvedType === 'COLOR' && typeof value === 'string' ? hexToRgba(value) : value;
|
|
499
|
+
variable.setValueForMode(modeId, processed);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
results.push({ success: true, name: v.name, id: variable.id });
|
|
503
|
+
} catch (err) {
|
|
504
|
+
results.push({ success: false, name: v.name, error: String(err) });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
created: results.filter(r => r.success).length,
|
|
510
|
+
failed: results.filter(r => !r.success).length,
|
|
511
|
+
results
|
|
512
|
+
};`;
|
|
513
|
+
const timeout = Math.max(5000, variables.length * 200);
|
|
514
|
+
const result = await connector.executeCodeViaUI(script, Math.min(timeout, 30000));
|
|
515
|
+
if (result.error) {
|
|
516
|
+
return {
|
|
517
|
+
content: [
|
|
518
|
+
{
|
|
519
|
+
type: "text",
|
|
520
|
+
text: JSON.stringify({
|
|
521
|
+
error: result.error,
|
|
522
|
+
message: "Batch create failed during execution",
|
|
523
|
+
hint: "Check that the collection ID is valid and the Desktop Bridge plugin is running",
|
|
524
|
+
}),
|
|
525
|
+
},
|
|
526
|
+
],
|
|
527
|
+
isError: true,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
content: [
|
|
532
|
+
{
|
|
533
|
+
type: "text",
|
|
534
|
+
text: JSON.stringify({
|
|
535
|
+
success: true,
|
|
536
|
+
message: `Batch created ${result.result?.created ?? 0} variables (${result.result?.failed ?? 0} failed)`,
|
|
537
|
+
...result.result,
|
|
538
|
+
timestamp: Date.now(),
|
|
539
|
+
}),
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
logger.error({ error }, "Failed to batch create variables");
|
|
546
|
+
return {
|
|
547
|
+
content: [
|
|
548
|
+
{
|
|
549
|
+
type: "text",
|
|
550
|
+
text: JSON.stringify({
|
|
551
|
+
error: error instanceof Error
|
|
552
|
+
? error.message
|
|
553
|
+
: String(error),
|
|
554
|
+
message: "Failed to batch create variables",
|
|
555
|
+
hint: "Make sure the Desktop Bridge plugin is running and the collection ID is correct",
|
|
556
|
+
}),
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
isError: true,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
// Tool: Batch update variables
|
|
564
|
+
server.tool("figma_batch_update_variables", "Update multiple variable values in one operation. Use instead of calling figma_update_variable repeatedly — up to 50x faster for bulk updates. Get variable/mode IDs from figma_get_variables first. Requires Desktop Bridge plugin.", {
|
|
565
|
+
updates: z
|
|
566
|
+
.array(z.object({
|
|
567
|
+
variableId: z
|
|
568
|
+
.string()
|
|
569
|
+
.describe("Variable ID (e.g., 'VariableID:123:456')"),
|
|
570
|
+
modeId: z
|
|
571
|
+
.string()
|
|
572
|
+
.describe("Mode ID (e.g., '1:0')"),
|
|
573
|
+
value: z
|
|
574
|
+
.union([z.string(), z.number(), z.boolean()])
|
|
575
|
+
.describe("New value. COLOR: hex like '#FF0000'. FLOAT: number. STRING: text. BOOLEAN: true/false."),
|
|
576
|
+
}))
|
|
577
|
+
.min(1)
|
|
578
|
+
.max(100)
|
|
579
|
+
.describe("Array of updates to apply (1-100)"),
|
|
580
|
+
}, async ({ updates }) => {
|
|
581
|
+
try {
|
|
582
|
+
const connector = await getDesktopConnector();
|
|
583
|
+
const script = `
|
|
584
|
+
const results = [];
|
|
585
|
+
const updates = ${JSON.stringify(updates)};
|
|
586
|
+
|
|
587
|
+
function hexToRgba(hex) {
|
|
588
|
+
hex = hex.replace('#', '');
|
|
589
|
+
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
|
|
590
|
+
return {
|
|
591
|
+
r: parseInt(hex.substring(0, 2), 16) / 255,
|
|
592
|
+
g: parseInt(hex.substring(2, 4), 16) / 255,
|
|
593
|
+
b: parseInt(hex.substring(4, 6), 16) / 255,
|
|
594
|
+
a: hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
for (const u of updates) {
|
|
599
|
+
try {
|
|
600
|
+
const variable = await figma.variables.getVariableByIdAsync(u.variableId);
|
|
601
|
+
if (!variable) throw new Error('Variable not found: ' + u.variableId);
|
|
602
|
+
const isColor = variable.resolvedType === 'COLOR';
|
|
603
|
+
const processed = isColor && typeof u.value === 'string' ? hexToRgba(u.value) : u.value;
|
|
604
|
+
variable.setValueForMode(u.modeId, processed);
|
|
605
|
+
results.push({ success: true, variableId: u.variableId, name: variable.name });
|
|
606
|
+
} catch (err) {
|
|
607
|
+
results.push({ success: false, variableId: u.variableId, error: String(err) });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
updated: results.filter(r => r.success).length,
|
|
613
|
+
failed: results.filter(r => !r.success).length,
|
|
614
|
+
results
|
|
615
|
+
};`;
|
|
616
|
+
const timeout = Math.max(5000, updates.length * 150);
|
|
617
|
+
const result = await connector.executeCodeViaUI(script, Math.min(timeout, 30000));
|
|
618
|
+
if (result.error) {
|
|
619
|
+
return {
|
|
620
|
+
content: [
|
|
621
|
+
{
|
|
622
|
+
type: "text",
|
|
623
|
+
text: JSON.stringify({
|
|
624
|
+
error: result.error,
|
|
625
|
+
message: "Batch update failed during execution",
|
|
626
|
+
hint: "Check that variable IDs and mode IDs are valid",
|
|
627
|
+
}),
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
isError: true,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
content: [
|
|
635
|
+
{
|
|
636
|
+
type: "text",
|
|
637
|
+
text: JSON.stringify({
|
|
638
|
+
success: true,
|
|
639
|
+
message: `Batch updated ${result.result?.updated ?? 0} variables (${result.result?.failed ?? 0} failed)`,
|
|
640
|
+
...result.result,
|
|
641
|
+
timestamp: Date.now(),
|
|
642
|
+
}),
|
|
643
|
+
},
|
|
644
|
+
],
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
logger.error({ error }, "Failed to batch update variables");
|
|
649
|
+
return {
|
|
650
|
+
content: [
|
|
651
|
+
{
|
|
652
|
+
type: "text",
|
|
653
|
+
text: JSON.stringify({
|
|
654
|
+
error: error instanceof Error
|
|
655
|
+
? error.message
|
|
656
|
+
: String(error),
|
|
657
|
+
message: "Failed to batch update variables",
|
|
658
|
+
hint: "Make sure the Desktop Bridge plugin is running and variable/mode IDs are correct",
|
|
659
|
+
}),
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
isError: true,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
// Tool: Setup design tokens (collection + modes + variables atomically)
|
|
667
|
+
server.tool("figma_setup_design_tokens", "Create a complete design token structure in one operation: collection, modes, and all variables. Ideal for importing CSS custom properties or design tokens into Figma. Requires Desktop Bridge plugin.", {
|
|
668
|
+
collectionName: z
|
|
669
|
+
.string()
|
|
670
|
+
.describe("Name for the token collection (e.g., 'Brand Tokens')"),
|
|
671
|
+
modes: z
|
|
672
|
+
.array(z.string())
|
|
673
|
+
.min(1)
|
|
674
|
+
.max(4)
|
|
675
|
+
.describe("Mode names (first becomes default). Example: ['Light', 'Dark']"),
|
|
676
|
+
tokens: z
|
|
677
|
+
.array(z.object({
|
|
678
|
+
name: z
|
|
679
|
+
.string()
|
|
680
|
+
.describe("Token name (e.g., 'color/primary')"),
|
|
681
|
+
resolvedType: z
|
|
682
|
+
.enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"])
|
|
683
|
+
.describe("Token type"),
|
|
684
|
+
description: z
|
|
685
|
+
.string()
|
|
686
|
+
.optional()
|
|
687
|
+
.describe("Optional description"),
|
|
688
|
+
values: z
|
|
689
|
+
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
|
690
|
+
.describe("Values keyed by mode NAME (not ID). Example: { 'Light': '#FFFFFF', 'Dark': '#000000' }"),
|
|
691
|
+
}))
|
|
692
|
+
.min(1)
|
|
693
|
+
.max(100)
|
|
694
|
+
.describe("Token definitions (1-100)"),
|
|
695
|
+
}, async ({ collectionName, modes, tokens }) => {
|
|
696
|
+
try {
|
|
697
|
+
const connector = await getDesktopConnector();
|
|
698
|
+
const script = `
|
|
699
|
+
const collectionName = ${JSON.stringify(collectionName)};
|
|
700
|
+
const modeNames = ${JSON.stringify(modes)};
|
|
701
|
+
const tokenDefs = ${JSON.stringify(tokens)};
|
|
702
|
+
|
|
703
|
+
function hexToRgba(hex) {
|
|
704
|
+
hex = hex.replace('#', '');
|
|
705
|
+
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
|
|
706
|
+
return {
|
|
707
|
+
r: parseInt(hex.substring(0, 2), 16) / 255,
|
|
708
|
+
g: parseInt(hex.substring(2, 4), 16) / 255,
|
|
709
|
+
b: parseInt(hex.substring(4, 6), 16) / 255,
|
|
710
|
+
a: hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Step 1: Create collection
|
|
715
|
+
const collection = figma.variables.createVariableCollection(collectionName);
|
|
716
|
+
const modeMap = {};
|
|
717
|
+
|
|
718
|
+
// Step 2: Set up modes - first mode uses the default mode that was auto-created
|
|
719
|
+
const defaultModeId = collection.modes[0].modeId;
|
|
720
|
+
collection.renameMode(defaultModeId, modeNames[0]);
|
|
721
|
+
modeMap[modeNames[0]] = defaultModeId;
|
|
722
|
+
|
|
723
|
+
for (let i = 1; i < modeNames.length; i++) {
|
|
724
|
+
const newModeId = collection.addMode(modeNames[i]);
|
|
725
|
+
modeMap[modeNames[i]] = newModeId;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Step 3: Create all variables with values
|
|
729
|
+
const results = [];
|
|
730
|
+
for (const t of tokenDefs) {
|
|
731
|
+
try {
|
|
732
|
+
const variable = figma.variables.createVariable(t.name, collection, t.resolvedType);
|
|
733
|
+
if (t.description) variable.description = t.description;
|
|
734
|
+
for (const [modeName, value] of Object.entries(t.values)) {
|
|
735
|
+
const modeId = modeMap[modeName];
|
|
736
|
+
if (!modeId) { results.push({ success: false, name: t.name, error: 'Unknown mode: ' + modeName }); continue; }
|
|
737
|
+
const processed = t.resolvedType === 'COLOR' && typeof value === 'string' ? hexToRgba(value) : value;
|
|
738
|
+
variable.setValueForMode(modeId, processed);
|
|
739
|
+
}
|
|
740
|
+
results.push({ success: true, name: t.name, id: variable.id });
|
|
741
|
+
} catch (err) {
|
|
742
|
+
results.push({ success: false, name: t.name, error: String(err) });
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
collectionId: collection.id,
|
|
748
|
+
collectionName: collectionName,
|
|
749
|
+
modes: modeMap,
|
|
750
|
+
created: results.filter(r => r.success).length,
|
|
751
|
+
failed: results.filter(r => !r.success).length,
|
|
752
|
+
results
|
|
753
|
+
};`;
|
|
754
|
+
const timeout = Math.max(10000, tokens.length * 200 + modes.length * 500);
|
|
755
|
+
const result = await connector.executeCodeViaUI(script, Math.min(timeout, 30000));
|
|
756
|
+
if (result.error) {
|
|
757
|
+
return {
|
|
758
|
+
content: [
|
|
759
|
+
{
|
|
760
|
+
type: "text",
|
|
761
|
+
text: JSON.stringify({
|
|
762
|
+
error: result.error,
|
|
763
|
+
message: "Design token setup failed during execution",
|
|
764
|
+
hint: "Check the token definitions and ensure the Desktop Bridge plugin is running",
|
|
765
|
+
}),
|
|
766
|
+
},
|
|
767
|
+
],
|
|
768
|
+
isError: true,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
content: [
|
|
773
|
+
{
|
|
774
|
+
type: "text",
|
|
775
|
+
text: JSON.stringify({
|
|
776
|
+
success: true,
|
|
777
|
+
message: `Created collection "${collectionName}" with ${modes.length} mode(s) and ${result.result?.created ?? 0} tokens`,
|
|
778
|
+
...result.result,
|
|
779
|
+
timestamp: Date.now(),
|
|
780
|
+
}),
|
|
781
|
+
},
|
|
782
|
+
],
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
logger.error({ error }, "Failed to setup design tokens");
|
|
787
|
+
return {
|
|
788
|
+
content: [
|
|
789
|
+
{
|
|
790
|
+
type: "text",
|
|
791
|
+
text: JSON.stringify({
|
|
792
|
+
error: error instanceof Error
|
|
793
|
+
? error.message
|
|
794
|
+
: String(error),
|
|
795
|
+
message: "Failed to setup design tokens",
|
|
796
|
+
hint: "Make sure the Desktop Bridge plugin is running in Figma",
|
|
797
|
+
}),
|
|
798
|
+
},
|
|
799
|
+
],
|
|
800
|
+
isError: true,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
// ============================================================================
|
|
805
|
+
// COMPONENT INSTANTIATION & PROPERTY TOOLS
|
|
806
|
+
// ============================================================================
|
|
807
|
+
// Tool: Instantiate Component
|
|
808
|
+
server.tool("figma_instantiate_component", `Create an instance of a component from the design system.
|
|
809
|
+
|
|
810
|
+
**CRITICAL: Always pass BOTH componentKey AND nodeId together!**
|
|
811
|
+
Search results return both identifiers. Pass both so the tool can automatically fall back to nodeId if the component isn't published to a library. Most local/unpublished components require nodeId.
|
|
812
|
+
|
|
813
|
+
**IMPORTANT: Always re-search before instantiating!**
|
|
814
|
+
NodeIds are session-specific and may be stale from previous conversations. ALWAYS search for components at the start of each design session to get current, valid identifiers.
|
|
815
|
+
|
|
816
|
+
**VISUAL VALIDATION WORKFLOW:**
|
|
817
|
+
After instantiating components, use figma_take_screenshot to verify the result looks correct. Check placement, sizing, and visual balance.`, {
|
|
818
|
+
componentKey: z
|
|
819
|
+
.string()
|
|
820
|
+
.optional()
|
|
821
|
+
.describe("The component key from search results. Pass this WITH nodeId for automatic fallback."),
|
|
822
|
+
nodeId: z
|
|
823
|
+
.string()
|
|
824
|
+
.optional()
|
|
825
|
+
.describe("The node ID from search results. ALWAYS pass this alongside componentKey - most local components need it."),
|
|
826
|
+
variant: z
|
|
827
|
+
.record(z.string())
|
|
828
|
+
.optional()
|
|
829
|
+
.describe("Variant properties to set (e.g., { Type: 'Simple', State: 'Active' })"),
|
|
830
|
+
overrides: z
|
|
831
|
+
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
|
832
|
+
.optional()
|
|
833
|
+
.describe("Property overrides (e.g., { 'Button Label': 'Click Me' })"),
|
|
834
|
+
position: z
|
|
835
|
+
.object({
|
|
836
|
+
x: z.number(),
|
|
837
|
+
y: z.number(),
|
|
838
|
+
})
|
|
839
|
+
.optional()
|
|
840
|
+
.describe("Position on canvas (default: 0, 0)"),
|
|
841
|
+
parentId: z
|
|
842
|
+
.string()
|
|
843
|
+
.optional()
|
|
844
|
+
.describe("Parent node ID to append the instance to"),
|
|
845
|
+
}, async ({ componentKey, nodeId, variant, overrides, position, parentId, }) => {
|
|
846
|
+
try {
|
|
847
|
+
if (!componentKey && !nodeId) {
|
|
848
|
+
throw new Error("Either componentKey or nodeId is required");
|
|
849
|
+
}
|
|
850
|
+
const connector = await getDesktopConnector();
|
|
851
|
+
const result = await connector.instantiateComponent(componentKey || "", {
|
|
852
|
+
nodeId,
|
|
853
|
+
position,
|
|
854
|
+
overrides,
|
|
855
|
+
variant,
|
|
856
|
+
parentId,
|
|
857
|
+
});
|
|
858
|
+
if (!result.success) {
|
|
859
|
+
throw new Error(result.error || "Failed to instantiate component");
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
content: [
|
|
863
|
+
{
|
|
864
|
+
type: "text",
|
|
865
|
+
text: JSON.stringify({
|
|
866
|
+
success: true,
|
|
867
|
+
message: "Component instantiated successfully",
|
|
868
|
+
instance: result.instance,
|
|
869
|
+
timestamp: Date.now(),
|
|
870
|
+
}),
|
|
871
|
+
},
|
|
872
|
+
],
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
catch (error) {
|
|
876
|
+
logger.error({ error }, "Failed to instantiate component");
|
|
877
|
+
return {
|
|
878
|
+
content: [
|
|
879
|
+
{
|
|
880
|
+
type: "text",
|
|
881
|
+
text: JSON.stringify({
|
|
882
|
+
error: error instanceof Error ? error.message : String(error),
|
|
883
|
+
message: "Failed to instantiate component",
|
|
884
|
+
hint: "Make sure the component key is correct and the Desktop Bridge plugin is running",
|
|
885
|
+
}),
|
|
886
|
+
},
|
|
887
|
+
],
|
|
888
|
+
isError: true,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
// ============================================================================
|
|
893
|
+
// Component Property Management Tools
|
|
894
|
+
// ============================================================================
|
|
895
|
+
// Tool: Set Node Description
|
|
896
|
+
server.tool("figma_set_description", "Set the description text on a component, component set, or style. Descriptions appear in Dev Mode and help document design intent. Supports plain text and markdown formatting.", {
|
|
897
|
+
nodeId: z
|
|
898
|
+
.string()
|
|
899
|
+
.describe("The node ID of the component or style to update (e.g., '123:456')"),
|
|
900
|
+
description: z.string().describe("The plain text description to set"),
|
|
901
|
+
descriptionMarkdown: z
|
|
902
|
+
.string()
|
|
903
|
+
.optional()
|
|
904
|
+
.describe("Optional rich text description using markdown formatting"),
|
|
905
|
+
}, async ({ nodeId, description, descriptionMarkdown }) => {
|
|
906
|
+
try {
|
|
907
|
+
const connector = await getDesktopConnector();
|
|
908
|
+
const result = await connector.setNodeDescription(nodeId, description, descriptionMarkdown);
|
|
909
|
+
if (!result.success) {
|
|
910
|
+
throw new Error(result.error || "Failed to set description");
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
content: [
|
|
914
|
+
{
|
|
915
|
+
type: "text",
|
|
916
|
+
text: JSON.stringify({
|
|
917
|
+
success: true,
|
|
918
|
+
message: "Description set successfully",
|
|
919
|
+
node: result.node,
|
|
920
|
+
}),
|
|
921
|
+
},
|
|
922
|
+
],
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
logger.error({ error }, "Failed to set description");
|
|
927
|
+
return {
|
|
928
|
+
content: [
|
|
929
|
+
{
|
|
930
|
+
type: "text",
|
|
931
|
+
text: JSON.stringify({
|
|
932
|
+
error: error instanceof Error ? error.message : String(error),
|
|
933
|
+
hint: "Make sure the node supports descriptions (components, component sets, styles)",
|
|
934
|
+
}),
|
|
935
|
+
},
|
|
936
|
+
],
|
|
937
|
+
isError: true,
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
// Tool: Add Component Property
|
|
942
|
+
server.tool("figma_add_component_property", "Add a new component property to a component or component set. Properties enable dynamic content and behavior in component instances. Supported types: BOOLEAN (toggle), TEXT (string), INSTANCE_SWAP (component swap), VARIANT (variant selection).", {
|
|
943
|
+
nodeId: z.string().describe("The component or component set node ID"),
|
|
944
|
+
propertyName: z
|
|
945
|
+
.string()
|
|
946
|
+
.describe("Name for the new property (e.g., 'Show Icon', 'Button Label')"),
|
|
947
|
+
type: z
|
|
948
|
+
.enum(["BOOLEAN", "TEXT", "INSTANCE_SWAP", "VARIANT"])
|
|
949
|
+
.describe("Property type: BOOLEAN for toggles, TEXT for strings, INSTANCE_SWAP for component swaps, VARIANT for variant selection"),
|
|
950
|
+
defaultValue: z
|
|
951
|
+
.union([z.string(), z.number(), z.boolean()])
|
|
952
|
+
.describe("Default value for the property. BOOLEAN: true/false, TEXT: string, INSTANCE_SWAP: component key, VARIANT: variant value"),
|
|
953
|
+
}, async ({ nodeId, propertyName, type, defaultValue }) => {
|
|
954
|
+
try {
|
|
955
|
+
const connector = await getDesktopConnector();
|
|
956
|
+
const result = await connector.addComponentProperty(nodeId, propertyName, type, defaultValue);
|
|
957
|
+
if (!result.success) {
|
|
958
|
+
throw new Error(result.error || "Failed to add property");
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
content: [
|
|
962
|
+
{
|
|
963
|
+
type: "text",
|
|
964
|
+
text: JSON.stringify({
|
|
965
|
+
success: true,
|
|
966
|
+
message: "Component property added",
|
|
967
|
+
propertyName: result.propertyName,
|
|
968
|
+
hint: "The property name includes a unique suffix (e.g., 'Show Icon#123:456'). Use the full name for editing/deleting.",
|
|
969
|
+
}),
|
|
970
|
+
},
|
|
971
|
+
],
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
logger.error({ error }, "Failed to add component property");
|
|
976
|
+
return {
|
|
977
|
+
content: [
|
|
978
|
+
{
|
|
979
|
+
type: "text",
|
|
980
|
+
text: JSON.stringify({
|
|
981
|
+
error: error instanceof Error ? error.message : String(error),
|
|
982
|
+
hint: "Cannot add properties to variant components. Add to the parent component set instead.",
|
|
983
|
+
}),
|
|
984
|
+
},
|
|
985
|
+
],
|
|
986
|
+
isError: true,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
// Tool: Edit Component Property
|
|
991
|
+
server.tool("figma_edit_component_property", "Edit an existing component property. Can change the name, default value, or preferred values (for INSTANCE_SWAP). Use the full property name including the unique suffix.", {
|
|
992
|
+
nodeId: z.string().describe("The component or component set node ID"),
|
|
993
|
+
propertyName: z
|
|
994
|
+
.string()
|
|
995
|
+
.describe("The full property name with suffix (e.g., 'Show Icon#123:456')"),
|
|
996
|
+
newValue: z
|
|
997
|
+
.object({
|
|
998
|
+
name: z.string().optional().describe("New name for the property"),
|
|
999
|
+
defaultValue: z
|
|
1000
|
+
.union([z.string(), z.number(), z.boolean()])
|
|
1001
|
+
.optional()
|
|
1002
|
+
.describe("New default value"),
|
|
1003
|
+
preferredValues: z
|
|
1004
|
+
.array(z.object({
|
|
1005
|
+
type: z
|
|
1006
|
+
.enum(["COMPONENT", "COMPONENT_SET"])
|
|
1007
|
+
.describe("Type of preferred value"),
|
|
1008
|
+
key: z.string().describe("Component or component set key"),
|
|
1009
|
+
}))
|
|
1010
|
+
.optional()
|
|
1011
|
+
.describe("Preferred values (INSTANCE_SWAP only)"),
|
|
1012
|
+
})
|
|
1013
|
+
.describe("Object with the values to update"),
|
|
1014
|
+
}, async ({ nodeId, propertyName, newValue }) => {
|
|
1015
|
+
try {
|
|
1016
|
+
const connector = await getDesktopConnector();
|
|
1017
|
+
const result = await connector.editComponentProperty(nodeId, propertyName, newValue);
|
|
1018
|
+
if (!result.success) {
|
|
1019
|
+
throw new Error(result.error || "Failed to edit property");
|
|
1020
|
+
}
|
|
1021
|
+
return {
|
|
1022
|
+
content: [
|
|
1023
|
+
{
|
|
1024
|
+
type: "text",
|
|
1025
|
+
text: JSON.stringify({
|
|
1026
|
+
success: true,
|
|
1027
|
+
message: "Component property updated",
|
|
1028
|
+
propertyName: result.propertyName,
|
|
1029
|
+
}),
|
|
1030
|
+
},
|
|
1031
|
+
],
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
catch (error) {
|
|
1035
|
+
logger.error({ error }, "Failed to edit component property");
|
|
1036
|
+
return {
|
|
1037
|
+
content: [
|
|
1038
|
+
{
|
|
1039
|
+
type: "text",
|
|
1040
|
+
text: JSON.stringify({
|
|
1041
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1042
|
+
}),
|
|
1043
|
+
},
|
|
1044
|
+
],
|
|
1045
|
+
isError: true,
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
// Tool: Delete Component Property
|
|
1050
|
+
server.tool("figma_delete_component_property", "Delete a component property. Only works with BOOLEAN, TEXT, and INSTANCE_SWAP properties (not VARIANT). This is a destructive operation.", {
|
|
1051
|
+
nodeId: z.string().describe("The component or component set node ID"),
|
|
1052
|
+
propertyName: z
|
|
1053
|
+
.string()
|
|
1054
|
+
.describe("The full property name with suffix (e.g., 'Show Icon#123:456')"),
|
|
1055
|
+
}, async ({ nodeId, propertyName }) => {
|
|
1056
|
+
try {
|
|
1057
|
+
const connector = await getDesktopConnector();
|
|
1058
|
+
const result = await connector.deleteComponentProperty(nodeId, propertyName);
|
|
1059
|
+
if (!result.success) {
|
|
1060
|
+
throw new Error(result.error || "Failed to delete property");
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
content: [
|
|
1064
|
+
{
|
|
1065
|
+
type: "text",
|
|
1066
|
+
text: JSON.stringify({
|
|
1067
|
+
success: true,
|
|
1068
|
+
message: "Component property deleted",
|
|
1069
|
+
}),
|
|
1070
|
+
},
|
|
1071
|
+
],
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
catch (error) {
|
|
1075
|
+
logger.error({ error }, "Failed to delete component property");
|
|
1076
|
+
return {
|
|
1077
|
+
content: [
|
|
1078
|
+
{
|
|
1079
|
+
type: "text",
|
|
1080
|
+
text: JSON.stringify({
|
|
1081
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1082
|
+
hint: "Cannot delete VARIANT properties. Only BOOLEAN, TEXT, and INSTANCE_SWAP can be deleted.",
|
|
1083
|
+
}),
|
|
1084
|
+
},
|
|
1085
|
+
],
|
|
1086
|
+
isError: true,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
// ============================================================================
|
|
1091
|
+
// Node Manipulation Tools
|
|
1092
|
+
// ============================================================================
|
|
1093
|
+
// Tool: Resize Node
|
|
1094
|
+
server.tool("figma_resize_node", "Resize a node to specific dimensions. By default respects child constraints; use withConstraints=false to ignore them.", {
|
|
1095
|
+
nodeId: z.string().describe("The node ID to resize"),
|
|
1096
|
+
width: z.number().describe("New width in pixels"),
|
|
1097
|
+
height: z.number().describe("New height in pixels"),
|
|
1098
|
+
withConstraints: z
|
|
1099
|
+
.boolean()
|
|
1100
|
+
.optional()
|
|
1101
|
+
.default(true)
|
|
1102
|
+
.describe("Whether to apply child constraints during resize (default: true)"),
|
|
1103
|
+
}, async ({ nodeId, width, height, withConstraints }) => {
|
|
1104
|
+
try {
|
|
1105
|
+
const connector = await getDesktopConnector();
|
|
1106
|
+
const result = await connector.resizeNode(nodeId, width, height, withConstraints);
|
|
1107
|
+
if (!result.success) {
|
|
1108
|
+
throw new Error(result.error || "Failed to resize node");
|
|
1109
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
content: [
|
|
1112
|
+
{
|
|
1113
|
+
type: "text",
|
|
1114
|
+
text: JSON.stringify({
|
|
1115
|
+
success: true,
|
|
1116
|
+
message: `Node resized to ${width}x${height}`,
|
|
1117
|
+
node: result.node,
|
|
1118
|
+
}),
|
|
1119
|
+
},
|
|
1120
|
+
],
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
catch (error) {
|
|
1124
|
+
logger.error({ error }, "Failed to resize node");
|
|
1125
|
+
return {
|
|
1126
|
+
content: [
|
|
1127
|
+
{
|
|
1128
|
+
type: "text",
|
|
1129
|
+
text: JSON.stringify({
|
|
1130
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1131
|
+
}),
|
|
1132
|
+
},
|
|
1133
|
+
],
|
|
1134
|
+
isError: true,
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
// Tool: Move Node
|
|
1139
|
+
server.tool("figma_move_node", "Move a node to a new position within its parent.", {
|
|
1140
|
+
nodeId: z.string().describe("The node ID to move"),
|
|
1141
|
+
x: z.number().describe("New X position"),
|
|
1142
|
+
y: z.number().describe("New Y position"),
|
|
1143
|
+
}, async ({ nodeId, x, y }) => {
|
|
1144
|
+
try {
|
|
1145
|
+
const connector = await getDesktopConnector();
|
|
1146
|
+
const result = await connector.moveNode(nodeId, x, y);
|
|
1147
|
+
if (!result.success) {
|
|
1148
|
+
throw new Error(result.error || "Failed to move node");
|
|
1149
|
+
}
|
|
1150
|
+
return {
|
|
1151
|
+
content: [
|
|
1152
|
+
{
|
|
1153
|
+
type: "text",
|
|
1154
|
+
text: JSON.stringify({
|
|
1155
|
+
success: true,
|
|
1156
|
+
message: `Node moved to (${x}, ${y})`,
|
|
1157
|
+
node: result.node,
|
|
1158
|
+
}),
|
|
1159
|
+
},
|
|
1160
|
+
],
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
catch (error) {
|
|
1164
|
+
logger.error({ error }, "Failed to move node");
|
|
1165
|
+
return {
|
|
1166
|
+
content: [
|
|
1167
|
+
{
|
|
1168
|
+
type: "text",
|
|
1169
|
+
text: JSON.stringify({
|
|
1170
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1171
|
+
}),
|
|
1172
|
+
},
|
|
1173
|
+
],
|
|
1174
|
+
isError: true,
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
// Tool: Set Node Fills
|
|
1179
|
+
server.tool("figma_set_fills", "Set the fill colors on a node. Accepts hex color strings (e.g., '#FF0000') or full paint objects.", {
|
|
1180
|
+
nodeId: z.string().describe("The node ID to modify"),
|
|
1181
|
+
fills: z
|
|
1182
|
+
.array(z.object({
|
|
1183
|
+
type: z
|
|
1184
|
+
.literal("SOLID")
|
|
1185
|
+
.describe("Fill type (currently only SOLID supported)"),
|
|
1186
|
+
color: z
|
|
1187
|
+
.string()
|
|
1188
|
+
.describe("Hex color string (e.g., '#FF0000', '#FF000080' for transparency)"),
|
|
1189
|
+
opacity: z
|
|
1190
|
+
.number()
|
|
1191
|
+
.optional()
|
|
1192
|
+
.describe("Opacity 0-1 (default: 1)"),
|
|
1193
|
+
}))
|
|
1194
|
+
.describe("Array of fill objects"),
|
|
1195
|
+
}, async ({ nodeId, fills }) => {
|
|
1196
|
+
try {
|
|
1197
|
+
const connector = await getDesktopConnector();
|
|
1198
|
+
const result = await connector.setNodeFills(nodeId, fills);
|
|
1199
|
+
if (!result.success) {
|
|
1200
|
+
throw new Error(result.error || "Failed to set fills");
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
content: [
|
|
1204
|
+
{
|
|
1205
|
+
type: "text",
|
|
1206
|
+
text: JSON.stringify({
|
|
1207
|
+
success: true,
|
|
1208
|
+
message: "Fills updated",
|
|
1209
|
+
node: result.node,
|
|
1210
|
+
}),
|
|
1211
|
+
},
|
|
1212
|
+
],
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
catch (error) {
|
|
1216
|
+
logger.error({ error }, "Failed to set fills");
|
|
1217
|
+
return {
|
|
1218
|
+
content: [
|
|
1219
|
+
{
|
|
1220
|
+
type: "text",
|
|
1221
|
+
text: JSON.stringify({
|
|
1222
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1223
|
+
}),
|
|
1224
|
+
},
|
|
1225
|
+
],
|
|
1226
|
+
isError: true,
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
// Tool: Set Image Fill on nodes
|
|
1231
|
+
server.tool("figma_set_image_fill", "Set an image fill on one or more Figma nodes. The imageData parameter accepts a base64-encoded " +
|
|
1232
|
+
"image string (JPEG/PNG). The image is decoded in the browser bridge and passed " +
|
|
1233
|
+
"as raw bytes to the Figma plugin. Requires Desktop Bridge plugin.", {
|
|
1234
|
+
nodeIds: z.array(z.string()).describe("Array of node IDs to apply the image fill to"),
|
|
1235
|
+
imageData: z.string().describe("Base64-encoded image data (JPEG/PNG)"),
|
|
1236
|
+
scaleMode: z.enum(["FILL", "FIT", "CROP", "TILE"]).optional().describe("How the image fills the node (default: FILL)"),
|
|
1237
|
+
}, async ({ nodeIds, imageData, scaleMode }) => {
|
|
1238
|
+
try {
|
|
1239
|
+
const connector = await getDesktopConnector();
|
|
1240
|
+
const result = await connector.setImageFill(nodeIds, imageData, scaleMode || "FILL");
|
|
1241
|
+
if (!result.success) {
|
|
1242
|
+
throw new Error(result.error || "Failed to set image fill");
|
|
1243
|
+
}
|
|
1244
|
+
return {
|
|
1245
|
+
content: [
|
|
1246
|
+
{
|
|
1247
|
+
type: "text",
|
|
1248
|
+
text: JSON.stringify({
|
|
1249
|
+
success: true,
|
|
1250
|
+
message: `Image fill applied to ${result.updatedCount || 0} node(s)`,
|
|
1251
|
+
imageHash: result.imageHash,
|
|
1252
|
+
nodes: result.nodes,
|
|
1253
|
+
}),
|
|
1254
|
+
},
|
|
1255
|
+
],
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
catch (error) {
|
|
1259
|
+
logger.error({ error }, "Failed to set image fill");
|
|
1260
|
+
return {
|
|
1261
|
+
content: [
|
|
1262
|
+
{
|
|
1263
|
+
type: "text",
|
|
1264
|
+
text: JSON.stringify({
|
|
1265
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1266
|
+
}),
|
|
1267
|
+
},
|
|
1268
|
+
],
|
|
1269
|
+
isError: true,
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
// Tool: Set Node Strokes
|
|
1274
|
+
server.tool("figma_set_strokes", "Set the stroke (border) on a node. Accepts hex color strings and optional stroke weight.", {
|
|
1275
|
+
nodeId: z.string().describe("The node ID to modify"),
|
|
1276
|
+
strokes: z
|
|
1277
|
+
.array(z.object({
|
|
1278
|
+
type: z.literal("SOLID").describe("Stroke type"),
|
|
1279
|
+
color: z.string().describe("Hex color string"),
|
|
1280
|
+
opacity: z.number().optional().describe("Opacity 0-1"),
|
|
1281
|
+
}))
|
|
1282
|
+
.describe("Array of stroke objects"),
|
|
1283
|
+
strokeWeight: z
|
|
1284
|
+
.number()
|
|
1285
|
+
.optional()
|
|
1286
|
+
.describe("Stroke thickness in pixels"),
|
|
1287
|
+
}, async ({ nodeId, strokes, strokeWeight }) => {
|
|
1288
|
+
try {
|
|
1289
|
+
const connector = await getDesktopConnector();
|
|
1290
|
+
const result = await connector.setNodeStrokes(nodeId, strokes, strokeWeight);
|
|
1291
|
+
if (!result.success) {
|
|
1292
|
+
throw new Error(result.error || "Failed to set strokes");
|
|
1293
|
+
}
|
|
1294
|
+
return {
|
|
1295
|
+
content: [
|
|
1296
|
+
{
|
|
1297
|
+
type: "text",
|
|
1298
|
+
text: JSON.stringify({
|
|
1299
|
+
success: true,
|
|
1300
|
+
message: "Strokes updated",
|
|
1301
|
+
node: result.node,
|
|
1302
|
+
}),
|
|
1303
|
+
},
|
|
1304
|
+
],
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
catch (error) {
|
|
1308
|
+
logger.error({ error }, "Failed to set strokes");
|
|
1309
|
+
return {
|
|
1310
|
+
content: [
|
|
1311
|
+
{
|
|
1312
|
+
type: "text",
|
|
1313
|
+
text: JSON.stringify({
|
|
1314
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1315
|
+
}),
|
|
1316
|
+
},
|
|
1317
|
+
],
|
|
1318
|
+
isError: true,
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
// Tool: Clone Node
|
|
1323
|
+
server.tool("figma_clone_node", "Duplicate a node. The clone is placed at a slight offset from the original.", {
|
|
1324
|
+
nodeId: z.string().describe("The node ID to clone"),
|
|
1325
|
+
}, async ({ nodeId }) => {
|
|
1326
|
+
try {
|
|
1327
|
+
const connector = await getDesktopConnector();
|
|
1328
|
+
const result = await connector.cloneNode(nodeId);
|
|
1329
|
+
if (!result.success) {
|
|
1330
|
+
throw new Error(result.error || "Failed to clone node");
|
|
1331
|
+
}
|
|
1332
|
+
return {
|
|
1333
|
+
content: [
|
|
1334
|
+
{
|
|
1335
|
+
type: "text",
|
|
1336
|
+
text: JSON.stringify({
|
|
1337
|
+
success: true,
|
|
1338
|
+
message: "Node cloned",
|
|
1339
|
+
clonedNode: result.node,
|
|
1340
|
+
}),
|
|
1341
|
+
},
|
|
1342
|
+
],
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
catch (error) {
|
|
1346
|
+
logger.error({ error }, "Failed to clone node");
|
|
1347
|
+
return {
|
|
1348
|
+
content: [
|
|
1349
|
+
{
|
|
1350
|
+
type: "text",
|
|
1351
|
+
text: JSON.stringify({
|
|
1352
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1353
|
+
}),
|
|
1354
|
+
},
|
|
1355
|
+
],
|
|
1356
|
+
isError: true,
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
// Tool: Delete Node
|
|
1361
|
+
server.tool("figma_delete_node", "Delete a node from the canvas. WARNING: This is a destructive operation (can be undone with Figma's undo).", {
|
|
1362
|
+
nodeId: z.string().describe("The node ID to delete"),
|
|
1363
|
+
}, async ({ nodeId }) => {
|
|
1364
|
+
try {
|
|
1365
|
+
const connector = await getDesktopConnector();
|
|
1366
|
+
const result = await connector.deleteNode(nodeId);
|
|
1367
|
+
if (!result.success) {
|
|
1368
|
+
throw new Error(result.error || "Failed to delete node");
|
|
1369
|
+
}
|
|
1370
|
+
return {
|
|
1371
|
+
content: [
|
|
1372
|
+
{
|
|
1373
|
+
type: "text",
|
|
1374
|
+
text: JSON.stringify({
|
|
1375
|
+
success: true,
|
|
1376
|
+
message: "Node deleted",
|
|
1377
|
+
deleted: result.deleted,
|
|
1378
|
+
}),
|
|
1379
|
+
},
|
|
1380
|
+
],
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
catch (error) {
|
|
1384
|
+
logger.error({ error }, "Failed to delete node");
|
|
1385
|
+
return {
|
|
1386
|
+
content: [
|
|
1387
|
+
{
|
|
1388
|
+
type: "text",
|
|
1389
|
+
text: JSON.stringify({
|
|
1390
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1391
|
+
}),
|
|
1392
|
+
},
|
|
1393
|
+
],
|
|
1394
|
+
isError: true,
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
// Tool: Rename Node
|
|
1399
|
+
server.tool("figma_rename_node", "Rename a node in the layer panel.", {
|
|
1400
|
+
nodeId: z.string().describe("The node ID to rename"),
|
|
1401
|
+
newName: z.string().describe("The new name for the node"),
|
|
1402
|
+
}, async ({ nodeId, newName }) => {
|
|
1403
|
+
try {
|
|
1404
|
+
const connector = await getDesktopConnector();
|
|
1405
|
+
const result = await connector.renameNode(nodeId, newName);
|
|
1406
|
+
if (!result.success) {
|
|
1407
|
+
throw new Error(result.error || "Failed to rename node");
|
|
1408
|
+
}
|
|
1409
|
+
return {
|
|
1410
|
+
content: [
|
|
1411
|
+
{
|
|
1412
|
+
type: "text",
|
|
1413
|
+
text: JSON.stringify({
|
|
1414
|
+
success: true,
|
|
1415
|
+
message: `Node renamed to "${newName}"`,
|
|
1416
|
+
node: result.node,
|
|
1417
|
+
}),
|
|
1418
|
+
},
|
|
1419
|
+
],
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
catch (error) {
|
|
1423
|
+
logger.error({ error }, "Failed to rename node");
|
|
1424
|
+
return {
|
|
1425
|
+
content: [
|
|
1426
|
+
{
|
|
1427
|
+
type: "text",
|
|
1428
|
+
text: JSON.stringify({
|
|
1429
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1430
|
+
}),
|
|
1431
|
+
},
|
|
1432
|
+
],
|
|
1433
|
+
isError: true,
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
// Tool: Set Text Content
|
|
1438
|
+
server.tool("figma_set_text", "Set the text content of a text node. Optionally adjust font size.", {
|
|
1439
|
+
nodeId: z.string().describe("The text node ID"),
|
|
1440
|
+
text: z.string().describe("The new text content"),
|
|
1441
|
+
fontSize: z.number().optional().describe("Optional font size to set"),
|
|
1442
|
+
}, async ({ nodeId, text, fontSize }) => {
|
|
1443
|
+
try {
|
|
1444
|
+
const connector = await getDesktopConnector();
|
|
1445
|
+
const result = await connector.setTextContent(nodeId, text, fontSize ? { fontSize } : undefined);
|
|
1446
|
+
if (!result.success) {
|
|
1447
|
+
throw new Error(result.error || "Failed to set text");
|
|
1448
|
+
}
|
|
1449
|
+
return {
|
|
1450
|
+
content: [
|
|
1451
|
+
{
|
|
1452
|
+
type: "text",
|
|
1453
|
+
text: JSON.stringify({
|
|
1454
|
+
success: true,
|
|
1455
|
+
message: "Text content updated",
|
|
1456
|
+
node: result.node,
|
|
1457
|
+
}),
|
|
1458
|
+
},
|
|
1459
|
+
],
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
catch (error) {
|
|
1463
|
+
logger.error({ error }, "Failed to set text content");
|
|
1464
|
+
return {
|
|
1465
|
+
content: [
|
|
1466
|
+
{
|
|
1467
|
+
type: "text",
|
|
1468
|
+
text: JSON.stringify({
|
|
1469
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1470
|
+
hint: "Make sure the node is a TEXT node",
|
|
1471
|
+
}),
|
|
1472
|
+
},
|
|
1473
|
+
],
|
|
1474
|
+
isError: true,
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
// Tool: Create Child Node
|
|
1479
|
+
server.tool("figma_create_child", "Create a new child node inside a parent container. Useful for adding shapes, text, or frames to existing structures.", {
|
|
1480
|
+
parentId: z.string().describe("The parent node ID"),
|
|
1481
|
+
nodeType: z
|
|
1482
|
+
.enum(["RECTANGLE", "ELLIPSE", "FRAME", "TEXT", "LINE"])
|
|
1483
|
+
.describe("Type of node to create"),
|
|
1484
|
+
properties: z
|
|
1485
|
+
.object({
|
|
1486
|
+
name: z.string().optional().describe("Name for the new node"),
|
|
1487
|
+
x: z.number().optional().describe("X position within parent"),
|
|
1488
|
+
y: z.number().optional().describe("Y position within parent"),
|
|
1489
|
+
width: z.number().optional().describe("Width (default: 100)"),
|
|
1490
|
+
height: z.number().optional().describe("Height (default: 100)"),
|
|
1491
|
+
fills: z
|
|
1492
|
+
.array(z.object({
|
|
1493
|
+
type: z.literal("SOLID"),
|
|
1494
|
+
color: z.string(),
|
|
1495
|
+
}))
|
|
1496
|
+
.optional()
|
|
1497
|
+
.describe("Fill colors (hex strings)"),
|
|
1498
|
+
text: z
|
|
1499
|
+
.string()
|
|
1500
|
+
.optional()
|
|
1501
|
+
.describe("Text content (for TEXT nodes only)"),
|
|
1502
|
+
})
|
|
1503
|
+
.optional()
|
|
1504
|
+
.describe("Properties for the new node"),
|
|
1505
|
+
}, async ({ parentId, nodeType, properties }) => {
|
|
1506
|
+
try {
|
|
1507
|
+
const connector = await getDesktopConnector();
|
|
1508
|
+
const result = await connector.createChildNode(parentId, nodeType, properties);
|
|
1509
|
+
if (!result.success) {
|
|
1510
|
+
throw new Error(result.error || "Failed to create node");
|
|
1511
|
+
}
|
|
1512
|
+
return {
|
|
1513
|
+
content: [
|
|
1514
|
+
{
|
|
1515
|
+
type: "text",
|
|
1516
|
+
text: JSON.stringify({
|
|
1517
|
+
success: true,
|
|
1518
|
+
message: `Created ${nodeType} node`,
|
|
1519
|
+
node: result.node,
|
|
1520
|
+
}),
|
|
1521
|
+
},
|
|
1522
|
+
],
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
catch (error) {
|
|
1526
|
+
logger.error({ error }, "Failed to create child node");
|
|
1527
|
+
return {
|
|
1528
|
+
content: [
|
|
1529
|
+
{
|
|
1530
|
+
type: "text",
|
|
1531
|
+
text: JSON.stringify({
|
|
1532
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1533
|
+
hint: "Make sure the parent node supports children (frames, groups, etc.)",
|
|
1534
|
+
}),
|
|
1535
|
+
},
|
|
1536
|
+
],
|
|
1537
|
+
isError: true,
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
// ============================================================================
|
|
1542
|
+
// Component Set Arrangement Tool
|
|
1543
|
+
// ============================================================================
|
|
1544
|
+
// Tool: Arrange Component Set (Professional Layout with Native Visualization)
|
|
1545
|
+
// Recreates component set using figma.combineAsVariants() for proper purple dashed frame
|
|
1546
|
+
server.tool("figma_arrange_component_set", `Organize a component set with Figma's native purple dashed visualization. Use after creating variants, adding states (hover/disabled/pressed), or when component sets need cleanup.
|
|
1547
|
+
|
|
1548
|
+
Recreates the set using figma.combineAsVariants() for proper Figma integration, applies purple dashed border styling, and arranges variants in a labeled grid (columns = last property like State, rows = other properties like Type+Size). Creates a white container with title, row/column labels, and the component set.`, {
|
|
1549
|
+
componentSetId: z
|
|
1550
|
+
.string()
|
|
1551
|
+
.optional()
|
|
1552
|
+
.describe("Node ID of the component set to arrange. If not provided, will look for a selected component set."),
|
|
1553
|
+
componentSetName: z
|
|
1554
|
+
.string()
|
|
1555
|
+
.optional()
|
|
1556
|
+
.describe("Name of the component set to find. Used if componentSetId not provided."),
|
|
1557
|
+
options: z
|
|
1558
|
+
.object({
|
|
1559
|
+
gap: z
|
|
1560
|
+
.number()
|
|
1561
|
+
.optional()
|
|
1562
|
+
.default(24)
|
|
1563
|
+
.describe("Gap between grid cells in pixels (default: 24)"),
|
|
1564
|
+
cellPadding: z
|
|
1565
|
+
.number()
|
|
1566
|
+
.optional()
|
|
1567
|
+
.default(20)
|
|
1568
|
+
.describe("Padding inside each cell around the variant (default: 20)"),
|
|
1569
|
+
columnProperty: z
|
|
1570
|
+
.string()
|
|
1571
|
+
.optional()
|
|
1572
|
+
.describe("Property to use for columns (default: auto-detect last property, usually 'State')"),
|
|
1573
|
+
})
|
|
1574
|
+
.optional()
|
|
1575
|
+
.describe("Layout options"),
|
|
1576
|
+
}, async ({ componentSetId, componentSetName, options }) => {
|
|
1577
|
+
try {
|
|
1578
|
+
const connector = await getDesktopConnector();
|
|
1579
|
+
// Build the code to execute in Figma
|
|
1580
|
+
const code = `
|
|
1581
|
+
// ============================================================================
|
|
1582
|
+
// COMPONENT SET ARRANGEMENT WITH PROPER LABELS AND CONTAINER
|
|
1583
|
+
// Creates: White container frame -> Row labels (left) -> Column headers (top) -> Component set (center)
|
|
1584
|
+
// Uses auto-layout for proper alignment of labels with grid cells
|
|
1585
|
+
// ============================================================================
|
|
1586
|
+
|
|
1587
|
+
// Configuration
|
|
1588
|
+
const config = ${JSON.stringify(options || {})};
|
|
1589
|
+
const gap = config.gap ?? 24;
|
|
1590
|
+
const cellPadding = config.cellPadding ?? 20;
|
|
1591
|
+
const columnProperty = config.columnProperty || null;
|
|
1592
|
+
|
|
1593
|
+
// Layout constants
|
|
1594
|
+
const LABEL_FONT_SIZE = 12;
|
|
1595
|
+
const LABEL_COLOR = { r: 0.4, g: 0.4, b: 0.4 }; // Gray text
|
|
1596
|
+
const TITLE_FONT_SIZE = 24;
|
|
1597
|
+
const TITLE_COLOR = { r: 0.1, g: 0.1, b: 0.1 }; // Dark text
|
|
1598
|
+
const CONTAINER_PADDING = 40;
|
|
1599
|
+
const LABEL_GAP = 16; // Gap between labels and component set
|
|
1600
|
+
const COLUMN_HEADER_HEIGHT = 32;
|
|
1601
|
+
|
|
1602
|
+
// Find the component set
|
|
1603
|
+
let componentSet = null;
|
|
1604
|
+
const csId = ${JSON.stringify(componentSetId || null)};
|
|
1605
|
+
const csName = ${JSON.stringify(componentSetName || null)};
|
|
1606
|
+
|
|
1607
|
+
if (csId) {
|
|
1608
|
+
componentSet = await figma.getNodeByIdAsync(csId);
|
|
1609
|
+
} else if (csName) {
|
|
1610
|
+
const allNodes = figma.currentPage.findAll(n => n.type === "COMPONENT_SET" && n.name === csName);
|
|
1611
|
+
componentSet = allNodes[0];
|
|
1612
|
+
} else {
|
|
1613
|
+
const selection = figma.currentPage.selection;
|
|
1614
|
+
componentSet = selection.find(n => n.type === "COMPONENT_SET");
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
if (!componentSet || componentSet.type !== "COMPONENT_SET") {
|
|
1618
|
+
return { error: "Component set not found. Provide componentSetId, componentSetName, or select a component set." };
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const page = figma.currentPage;
|
|
1622
|
+
const csOriginalX = componentSet.x;
|
|
1623
|
+
const csOriginalY = componentSet.y;
|
|
1624
|
+
const csOriginalName = componentSet.name;
|
|
1625
|
+
|
|
1626
|
+
// Get all variant components
|
|
1627
|
+
const variants = componentSet.children.filter(n => n.type === "COMPONENT");
|
|
1628
|
+
if (variants.length === 0) {
|
|
1629
|
+
return { error: "No variants found in component set" };
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// Parse variant properties from names
|
|
1633
|
+
const parseVariantName = (name) => {
|
|
1634
|
+
const props = {};
|
|
1635
|
+
const parts = name.split(", ");
|
|
1636
|
+
for (const part of parts) {
|
|
1637
|
+
const [key, value] = part.split("=");
|
|
1638
|
+
if (key && value) {
|
|
1639
|
+
props[key.trim()] = value.trim();
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return props;
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
// Collect all properties and their unique values (preserving order)
|
|
1646
|
+
const propertyValues = {};
|
|
1647
|
+
const propertyOrder = [];
|
|
1648
|
+
for (const variant of variants) {
|
|
1649
|
+
const props = parseVariantName(variant.name);
|
|
1650
|
+
for (const [key, value] of Object.entries(props)) {
|
|
1651
|
+
if (!propertyValues[key]) {
|
|
1652
|
+
propertyValues[key] = new Set();
|
|
1653
|
+
propertyOrder.push(key);
|
|
1654
|
+
}
|
|
1655
|
+
propertyValues[key].add(value);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
for (const key of Object.keys(propertyValues)) {
|
|
1659
|
+
propertyValues[key] = Array.from(propertyValues[key]);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// Determine grid structure: columns = last property (usually State), rows = other properties
|
|
1663
|
+
const columnProp = columnProperty || propertyOrder[propertyOrder.length - 1];
|
|
1664
|
+
const columnValues = propertyValues[columnProp] || [];
|
|
1665
|
+
const rowProps = propertyOrder.filter(p => p !== columnProp);
|
|
1666
|
+
|
|
1667
|
+
// Generate all row combinations
|
|
1668
|
+
const generateRowCombinations = (props, values) => {
|
|
1669
|
+
if (props.length === 0) return [{}];
|
|
1670
|
+
if (props.length === 1) {
|
|
1671
|
+
return values[props[0]].map(v => ({ [props[0]]: v }));
|
|
1672
|
+
}
|
|
1673
|
+
const result = [];
|
|
1674
|
+
const firstProp = props[0];
|
|
1675
|
+
const restProps = props.slice(1);
|
|
1676
|
+
const restCombos = generateRowCombinations(restProps, values);
|
|
1677
|
+
for (const value of values[firstProp]) {
|
|
1678
|
+
for (const combo of restCombos) {
|
|
1679
|
+
result.push({ [firstProp]: value, ...combo });
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return result;
|
|
1683
|
+
};
|
|
1684
|
+
const rowCombinations = generateRowCombinations(rowProps, propertyValues);
|
|
1685
|
+
|
|
1686
|
+
const totalCols = columnValues.length;
|
|
1687
|
+
const totalRows = rowCombinations.length;
|
|
1688
|
+
|
|
1689
|
+
// Calculate max variant dimensions
|
|
1690
|
+
let maxVariantWidth = 0;
|
|
1691
|
+
let maxVariantHeight = 0;
|
|
1692
|
+
for (const v of variants) {
|
|
1693
|
+
if (v.width > maxVariantWidth) maxVariantWidth = v.width;
|
|
1694
|
+
if (v.height > maxVariantHeight) maxVariantHeight = v.height;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// Calculate cell dimensions (each cell in the grid)
|
|
1698
|
+
const cellWidth = Math.ceil(maxVariantWidth + cellPadding);
|
|
1699
|
+
const cellHeight = Math.ceil(maxVariantHeight + cellPadding);
|
|
1700
|
+
|
|
1701
|
+
// Calculate component set dimensions
|
|
1702
|
+
const edgePadding = 24; // Padding inside component set
|
|
1703
|
+
const csWidth = (totalCols * cellWidth) + ((totalCols - 1) * gap) + (edgePadding * 2);
|
|
1704
|
+
const csHeight = (totalRows * cellHeight) + ((totalRows - 1) * gap) + (edgePadding * 2);
|
|
1705
|
+
|
|
1706
|
+
// ============================================================================
|
|
1707
|
+
// STEP 1: Remove old labels and container frames from previous arrangements
|
|
1708
|
+
// ============================================================================
|
|
1709
|
+
const oldElements = page.children.filter(n =>
|
|
1710
|
+
(n.type === "TEXT" && (n.name.startsWith("Row: ") || n.name.startsWith("Col: "))) ||
|
|
1711
|
+
(n.type === "FRAME" && (n.name === "Component Container" || n.name === "Row Labels" || n.name === "Column Headers"))
|
|
1712
|
+
);
|
|
1713
|
+
for (const el of oldElements) {
|
|
1714
|
+
el.remove();
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// ============================================================================
|
|
1718
|
+
// STEP 2: Clone variants and recreate component set with native visualization
|
|
1719
|
+
// ============================================================================
|
|
1720
|
+
const clonedVariants = [];
|
|
1721
|
+
for (const variant of variants) {
|
|
1722
|
+
const clone = variant.clone();
|
|
1723
|
+
page.appendChild(clone);
|
|
1724
|
+
clonedVariants.push(clone);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// Delete the old component set
|
|
1728
|
+
componentSet.remove();
|
|
1729
|
+
|
|
1730
|
+
// Recreate using figma.combineAsVariants() for native purple dashed frame
|
|
1731
|
+
const newComponentSet = figma.combineAsVariants(clonedVariants, page);
|
|
1732
|
+
newComponentSet.name = csOriginalName;
|
|
1733
|
+
|
|
1734
|
+
// Apply purple dashed border (Figma's native component set styling)
|
|
1735
|
+
newComponentSet.strokes = [{
|
|
1736
|
+
type: 'SOLID',
|
|
1737
|
+
color: { r: 151/255, g: 71/255, b: 255/255 } // Figma's purple: #9747FF
|
|
1738
|
+
}];
|
|
1739
|
+
newComponentSet.dashPattern = [10, 5];
|
|
1740
|
+
newComponentSet.strokeWeight = 1;
|
|
1741
|
+
newComponentSet.strokeAlign = "INSIDE";
|
|
1742
|
+
|
|
1743
|
+
// ============================================================================
|
|
1744
|
+
// STEP 3: Arrange variants in grid pattern inside component set
|
|
1745
|
+
// ============================================================================
|
|
1746
|
+
const newVariants = newComponentSet.children.filter(n => n.type === "COMPONENT");
|
|
1747
|
+
|
|
1748
|
+
for (const variant of newVariants) {
|
|
1749
|
+
const props = parseVariantName(variant.name);
|
|
1750
|
+
const colValue = props[columnProp];
|
|
1751
|
+
const colIdx = columnValues.indexOf(colValue);
|
|
1752
|
+
|
|
1753
|
+
// Find matching row
|
|
1754
|
+
let rowIdx = -1;
|
|
1755
|
+
for (let i = 0; i < rowCombinations.length; i++) {
|
|
1756
|
+
const combo = rowCombinations[i];
|
|
1757
|
+
let match = true;
|
|
1758
|
+
for (const [key, value] of Object.entries(combo)) {
|
|
1759
|
+
if (props[key] !== value) {
|
|
1760
|
+
match = false;
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
if (match) {
|
|
1765
|
+
rowIdx = i;
|
|
1766
|
+
break;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
if (colIdx >= 0 && rowIdx >= 0) {
|
|
1771
|
+
// Calculate cell position
|
|
1772
|
+
const cellX = edgePadding + colIdx * (cellWidth + gap);
|
|
1773
|
+
const cellY = edgePadding + rowIdx * (cellHeight + gap);
|
|
1774
|
+
|
|
1775
|
+
// Center variant within cell
|
|
1776
|
+
const variantX = Math.round(cellX + (cellWidth - variant.width) / 2);
|
|
1777
|
+
const variantY = Math.round(cellY + (cellHeight - variant.height) / 2);
|
|
1778
|
+
|
|
1779
|
+
variant.x = variantX;
|
|
1780
|
+
variant.y = variantY;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// Resize component set to fit grid
|
|
1785
|
+
newComponentSet.resize(csWidth, csHeight);
|
|
1786
|
+
|
|
1787
|
+
// ============================================================================
|
|
1788
|
+
// STEP 4: Create white container frame with proper structure
|
|
1789
|
+
// ============================================================================
|
|
1790
|
+
|
|
1791
|
+
// Load font for labels
|
|
1792
|
+
await figma.loadFontAsync({ family: "Inter", style: "Regular" });
|
|
1793
|
+
await figma.loadFontAsync({ family: "Inter", style: "Semi Bold" });
|
|
1794
|
+
|
|
1795
|
+
// Create the main container frame (white background)
|
|
1796
|
+
const containerFrame = figma.createFrame();
|
|
1797
|
+
containerFrame.name = "Component Container";
|
|
1798
|
+
containerFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]; // White
|
|
1799
|
+
containerFrame.cornerRadius = 8;
|
|
1800
|
+
containerFrame.layoutMode = 'VERTICAL';
|
|
1801
|
+
containerFrame.primaryAxisSizingMode = 'AUTO';
|
|
1802
|
+
containerFrame.counterAxisSizingMode = 'AUTO';
|
|
1803
|
+
containerFrame.paddingTop = CONTAINER_PADDING;
|
|
1804
|
+
containerFrame.paddingRight = CONTAINER_PADDING;
|
|
1805
|
+
containerFrame.paddingBottom = CONTAINER_PADDING;
|
|
1806
|
+
containerFrame.paddingLeft = CONTAINER_PADDING;
|
|
1807
|
+
containerFrame.itemSpacing = 24;
|
|
1808
|
+
|
|
1809
|
+
// Add title
|
|
1810
|
+
const titleText = figma.createText();
|
|
1811
|
+
titleText.name = "Title";
|
|
1812
|
+
titleText.characters = csOriginalName;
|
|
1813
|
+
titleText.fontSize = TITLE_FONT_SIZE;
|
|
1814
|
+
titleText.fontName = { family: "Inter", style: "Semi Bold" };
|
|
1815
|
+
titleText.fills = [{ type: 'SOLID', color: TITLE_COLOR }];
|
|
1816
|
+
// Append to parent FIRST, then set layoutSizing
|
|
1817
|
+
containerFrame.appendChild(titleText);
|
|
1818
|
+
titleText.layoutSizingHorizontal = 'HUG';
|
|
1819
|
+
titleText.layoutSizingVertical = 'HUG';
|
|
1820
|
+
|
|
1821
|
+
// Create content row (horizontal: row labels + grid column)
|
|
1822
|
+
const contentRow = figma.createFrame();
|
|
1823
|
+
contentRow.name = "Content Row";
|
|
1824
|
+
contentRow.fills = []; // Transparent
|
|
1825
|
+
contentRow.layoutMode = 'HORIZONTAL';
|
|
1826
|
+
contentRow.primaryAxisSizingMode = 'AUTO';
|
|
1827
|
+
contentRow.counterAxisSizingMode = 'AUTO';
|
|
1828
|
+
contentRow.itemSpacing = LABEL_GAP;
|
|
1829
|
+
contentRow.counterAxisAlignItems = 'MIN'; // Align to top
|
|
1830
|
+
containerFrame.appendChild(contentRow);
|
|
1831
|
+
|
|
1832
|
+
// ============================================================================
|
|
1833
|
+
// STEP 5: Create row labels column (left side)
|
|
1834
|
+
// ============================================================================
|
|
1835
|
+
const rowLabelsFrame = figma.createFrame();
|
|
1836
|
+
rowLabelsFrame.name = "Row Labels";
|
|
1837
|
+
rowLabelsFrame.fills = []; // Transparent
|
|
1838
|
+
rowLabelsFrame.layoutMode = 'VERTICAL';
|
|
1839
|
+
rowLabelsFrame.primaryAxisSizingMode = 'AUTO';
|
|
1840
|
+
rowLabelsFrame.counterAxisSizingMode = 'AUTO';
|
|
1841
|
+
rowLabelsFrame.counterAxisAlignItems = 'MAX'; // Right-align text
|
|
1842
|
+
rowLabelsFrame.itemSpacing = 0; // No spacing - we'll use fixed heights
|
|
1843
|
+
|
|
1844
|
+
// Add spacer for column headers alignment
|
|
1845
|
+
// Must account for: column header height + gap + component set's internal edgePadding
|
|
1846
|
+
const rowLabelSpacer = figma.createFrame();
|
|
1847
|
+
rowLabelSpacer.name = "Spacer";
|
|
1848
|
+
rowLabelSpacer.fills = [];
|
|
1849
|
+
rowLabelSpacer.resize(10, COLUMN_HEADER_HEIGHT + gap + edgePadding); // Align with first row inside component set
|
|
1850
|
+
rowLabelsFrame.appendChild(rowLabelSpacer);
|
|
1851
|
+
// IMPORTANT: Set layoutSizing AFTER appendChild (node must be in auto-layout parent first)
|
|
1852
|
+
rowLabelSpacer.layoutSizingVertical = 'FIXED';
|
|
1853
|
+
|
|
1854
|
+
// Create row labels - each with VERTICAL layout for direct vertical centering
|
|
1855
|
+
// Using VERTICAL layout: primaryAxis = vertical, counterAxis = horizontal
|
|
1856
|
+
// So primaryAxisAlignItems = 'CENTER' directly controls vertical centering
|
|
1857
|
+
for (let i = 0; i < rowCombinations.length; i++) {
|
|
1858
|
+
const combo = rowCombinations[i];
|
|
1859
|
+
const labelText = rowProps.map(p => combo[p]).join(" / ");
|
|
1860
|
+
const isLastRow = (i === rowCombinations.length - 1);
|
|
1861
|
+
|
|
1862
|
+
// Create a frame to hold the label with VERTICAL layout
|
|
1863
|
+
const rowLabelContainer = figma.createFrame();
|
|
1864
|
+
rowLabelContainer.name = "Row: " + labelText;
|
|
1865
|
+
rowLabelContainer.fills = [];
|
|
1866
|
+
rowLabelContainer.layoutMode = 'VERTICAL'; // VERTICAL so primaryAxis controls Y
|
|
1867
|
+
rowLabelContainer.primaryAxisSizingMode = 'FIXED'; // CRITICAL: Don't hug content, maintain fixed height
|
|
1868
|
+
rowLabelContainer.primaryAxisAlignItems = 'CENTER'; // CENTER = vertically centered within fixed height
|
|
1869
|
+
rowLabelContainer.counterAxisAlignItems = 'MAX'; // MAX = right-aligned horizontally
|
|
1870
|
+
|
|
1871
|
+
// Fixed height = cellHeight only (gap handled separately below)
|
|
1872
|
+
rowLabelContainer.resize(10, cellHeight);
|
|
1873
|
+
|
|
1874
|
+
const label = figma.createText();
|
|
1875
|
+
label.characters = labelText;
|
|
1876
|
+
label.fontSize = LABEL_FONT_SIZE;
|
|
1877
|
+
label.fontName = { family: "Inter", style: "Regular" };
|
|
1878
|
+
label.fills = [{ type: 'SOLID', color: LABEL_COLOR }];
|
|
1879
|
+
label.textAlignHorizontal = 'RIGHT';
|
|
1880
|
+
rowLabelContainer.appendChild(label);
|
|
1881
|
+
|
|
1882
|
+
// Append to parent FIRST, then set layoutSizing properties
|
|
1883
|
+
rowLabelsFrame.appendChild(rowLabelContainer);
|
|
1884
|
+
rowLabelContainer.layoutSizingHorizontal = 'HUG';
|
|
1885
|
+
rowLabelContainer.layoutSizingVertical = 'FIXED';
|
|
1886
|
+
|
|
1887
|
+
// Add gap spacer AFTER the row label (except for the last row)
|
|
1888
|
+
// This separates the gap from the centering calculation entirely
|
|
1889
|
+
if (!isLastRow) {
|
|
1890
|
+
const gapSpacer = figma.createFrame();
|
|
1891
|
+
gapSpacer.name = "Row Gap";
|
|
1892
|
+
gapSpacer.fills = [];
|
|
1893
|
+
gapSpacer.resize(1, gap);
|
|
1894
|
+
rowLabelsFrame.appendChild(gapSpacer);
|
|
1895
|
+
// Plain frames can only use FIXED or FILL (not HUG)
|
|
1896
|
+
gapSpacer.layoutSizingHorizontal = 'FIXED';
|
|
1897
|
+
gapSpacer.layoutSizingVertical = 'FIXED';
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
contentRow.appendChild(rowLabelsFrame);
|
|
1902
|
+
|
|
1903
|
+
// ============================================================================
|
|
1904
|
+
// STEP 6: Create grid column (column headers + component set)
|
|
1905
|
+
// ============================================================================
|
|
1906
|
+
const gridColumn = figma.createFrame();
|
|
1907
|
+
gridColumn.name = "Grid Column";
|
|
1908
|
+
gridColumn.fills = []; // Transparent
|
|
1909
|
+
gridColumn.layoutMode = 'VERTICAL';
|
|
1910
|
+
gridColumn.primaryAxisSizingMode = 'AUTO';
|
|
1911
|
+
gridColumn.counterAxisSizingMode = 'AUTO';
|
|
1912
|
+
gridColumn.itemSpacing = gap;
|
|
1913
|
+
|
|
1914
|
+
// Create column headers row
|
|
1915
|
+
const columnHeadersRow = figma.createFrame();
|
|
1916
|
+
columnHeadersRow.name = "Column Headers";
|
|
1917
|
+
columnHeadersRow.fills = [];
|
|
1918
|
+
columnHeadersRow.layoutMode = 'HORIZONTAL';
|
|
1919
|
+
columnHeadersRow.resize(csWidth, COLUMN_HEADER_HEIGHT);
|
|
1920
|
+
columnHeadersRow.itemSpacing = 0; // No spacing - we control widths precisely
|
|
1921
|
+
columnHeadersRow.paddingLeft = edgePadding; // Match component set edge padding
|
|
1922
|
+
columnHeadersRow.paddingRight = edgePadding;
|
|
1923
|
+
|
|
1924
|
+
// Create column header labels - each with width matching cell + gap
|
|
1925
|
+
for (let i = 0; i < columnValues.length; i++) {
|
|
1926
|
+
const colValue = columnValues[i];
|
|
1927
|
+
const isLastCol = (i === columnValues.length - 1);
|
|
1928
|
+
|
|
1929
|
+
const colHeaderContainer = figma.createFrame();
|
|
1930
|
+
colHeaderContainer.name = "Col: " + colValue;
|
|
1931
|
+
colHeaderContainer.fills = [];
|
|
1932
|
+
colHeaderContainer.layoutMode = 'HORIZONTAL';
|
|
1933
|
+
colHeaderContainer.primaryAxisAlignItems = 'CENTER'; // Center horizontally
|
|
1934
|
+
colHeaderContainer.counterAxisAlignItems = 'MAX'; // Align to bottom
|
|
1935
|
+
|
|
1936
|
+
// Set width to match cell + gap (except last column)
|
|
1937
|
+
// Use paddingRight to push the gap to the RIGHT of the centered text area
|
|
1938
|
+
const colWidth = isLastCol ? cellWidth : cellWidth + gap;
|
|
1939
|
+
colHeaderContainer.resize(colWidth, COLUMN_HEADER_HEIGHT);
|
|
1940
|
+
if (!isLastCol) {
|
|
1941
|
+
colHeaderContainer.paddingRight = gap; // Gap goes right, text centers in cellWidth
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
const label = figma.createText();
|
|
1945
|
+
label.characters = colValue;
|
|
1946
|
+
label.fontSize = LABEL_FONT_SIZE;
|
|
1947
|
+
label.fontName = { family: "Inter", style: "Regular" };
|
|
1948
|
+
label.fills = [{ type: 'SOLID', color: LABEL_COLOR }];
|
|
1949
|
+
label.textAlignHorizontal = 'CENTER';
|
|
1950
|
+
colHeaderContainer.appendChild(label);
|
|
1951
|
+
|
|
1952
|
+
// Append to parent FIRST, then set layoutSizing
|
|
1953
|
+
columnHeadersRow.appendChild(colHeaderContainer);
|
|
1954
|
+
colHeaderContainer.layoutSizingHorizontal = 'FIXED';
|
|
1955
|
+
colHeaderContainer.layoutSizingVertical = 'FILL';
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// Append to parent FIRST, then set layoutSizing
|
|
1959
|
+
gridColumn.appendChild(columnHeadersRow);
|
|
1960
|
+
columnHeadersRow.layoutSizingHorizontal = 'FIXED';
|
|
1961
|
+
columnHeadersRow.layoutSizingVertical = 'FIXED';
|
|
1962
|
+
|
|
1963
|
+
// Create a wrapper frame to hold the component set (since component sets don't work well in auto-layout)
|
|
1964
|
+
const componentSetWrapper = figma.createFrame();
|
|
1965
|
+
componentSetWrapper.name = "Component Set Wrapper";
|
|
1966
|
+
componentSetWrapper.fills = [];
|
|
1967
|
+
componentSetWrapper.resize(csWidth, csHeight);
|
|
1968
|
+
|
|
1969
|
+
// Move component set inside wrapper (positioned at 0,0)
|
|
1970
|
+
componentSetWrapper.appendChild(newComponentSet);
|
|
1971
|
+
newComponentSet.x = 0;
|
|
1972
|
+
newComponentSet.y = 0;
|
|
1973
|
+
|
|
1974
|
+
// Append to parent FIRST, then set layoutSizing
|
|
1975
|
+
gridColumn.appendChild(componentSetWrapper);
|
|
1976
|
+
componentSetWrapper.layoutSizingHorizontal = 'FIXED';
|
|
1977
|
+
componentSetWrapper.layoutSizingVertical = 'FIXED';
|
|
1978
|
+
|
|
1979
|
+
contentRow.appendChild(gridColumn);
|
|
1980
|
+
|
|
1981
|
+
// Position container at original location
|
|
1982
|
+
containerFrame.x = csOriginalX - CONTAINER_PADDING - 120; // Account for row labels width
|
|
1983
|
+
containerFrame.y = csOriginalY - CONTAINER_PADDING - TITLE_FONT_SIZE - 24 - COLUMN_HEADER_HEIGHT - gap;
|
|
1984
|
+
|
|
1985
|
+
// Select and zoom to show result
|
|
1986
|
+
figma.currentPage.selection = [containerFrame];
|
|
1987
|
+
figma.viewport.scrollAndZoomIntoView([containerFrame]);
|
|
1988
|
+
|
|
1989
|
+
return {
|
|
1990
|
+
success: true,
|
|
1991
|
+
message: "Component set arranged with proper container, labels, and alignment",
|
|
1992
|
+
containerId: containerFrame.id,
|
|
1993
|
+
componentSetId: newComponentSet.id,
|
|
1994
|
+
componentSetName: newComponentSet.name,
|
|
1995
|
+
grid: {
|
|
1996
|
+
rows: totalRows,
|
|
1997
|
+
columns: totalCols,
|
|
1998
|
+
cellWidth: cellWidth,
|
|
1999
|
+
cellHeight: cellHeight,
|
|
2000
|
+
gap: gap,
|
|
2001
|
+
columnProperty: columnProp,
|
|
2002
|
+
columnValues: columnValues,
|
|
2003
|
+
rowProperties: rowProps,
|
|
2004
|
+
rowLabels: rowCombinations.map(combo => rowProps.map(p => combo[p]).join(" / "))
|
|
2005
|
+
},
|
|
2006
|
+
componentSetSize: { width: csWidth, height: csHeight },
|
|
2007
|
+
variantCount: newVariants.length,
|
|
2008
|
+
structure: {
|
|
2009
|
+
container: "White frame with title, row labels, column headers, and component set",
|
|
2010
|
+
rowLabels: "Vertically aligned with each row's center",
|
|
2011
|
+
columnHeaders: "Horizontally aligned with each column's center"
|
|
2012
|
+
}
|
|
2013
|
+
};
|
|
2014
|
+
`;
|
|
2015
|
+
const result = await connector.executeCodeViaUI(code, 25000);
|
|
2016
|
+
if (!result.success) {
|
|
2017
|
+
throw new Error(result.error || "Failed to arrange component set");
|
|
2018
|
+
}
|
|
2019
|
+
return {
|
|
2020
|
+
content: [
|
|
2021
|
+
{
|
|
2022
|
+
type: "text",
|
|
2023
|
+
text: JSON.stringify({
|
|
2024
|
+
...result.result,
|
|
2025
|
+
hint: result.result?.success
|
|
2026
|
+
? "Component set arranged in a white container frame with properly aligned row and column labels. The purple dashed border is visible. Use figma_capture_screenshot to validate the layout."
|
|
2027
|
+
: undefined,
|
|
2028
|
+
}),
|
|
2029
|
+
},
|
|
2030
|
+
],
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
catch (error) {
|
|
2034
|
+
logger.error({ error }, "Failed to arrange component set");
|
|
2035
|
+
return {
|
|
2036
|
+
content: [
|
|
2037
|
+
{
|
|
2038
|
+
type: "text",
|
|
2039
|
+
text: JSON.stringify({
|
|
2040
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2041
|
+
hint: "Make sure the Desktop Bridge plugin is running and a component set exists.",
|
|
2042
|
+
}),
|
|
2043
|
+
},
|
|
2044
|
+
],
|
|
2045
|
+
isError: true,
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
// Tool: Lint Design for accessibility and quality issues
|
|
2050
|
+
server.tool("figma_lint_design", "Run accessibility (WCAG) and design quality checks on the current page or a specific node tree. " +
|
|
2051
|
+
"Checks color contrast ratios, text sizing, touch targets, hardcoded values, detached components, " +
|
|
2052
|
+
"naming conventions, and layout quality. Returns categorized findings with severity levels. " +
|
|
2053
|
+
"Use natural language like 'check my design for accessibility issues' or 'lint this page'. " +
|
|
2054
|
+
"Requires Desktop Bridge plugin.", {
|
|
2055
|
+
nodeId: z.string().optional().describe("Node ID to lint (defaults to current page)"),
|
|
2056
|
+
rules: z.array(z.string()).optional().describe("Rule filter: ['all'] (default), ['wcag'], ['design-system'], ['layout'], or specific rule IDs like ['wcag-contrast', 'detached-component']"),
|
|
2057
|
+
maxDepth: z.number().optional().describe("Maximum tree depth to traverse (default: 10)"),
|
|
2058
|
+
maxFindings: z.number().optional().describe("Maximum findings before stopping (default: 100)"),
|
|
2059
|
+
}, async ({ nodeId, rules, maxDepth, maxFindings }) => {
|
|
2060
|
+
try {
|
|
2061
|
+
const connector = await getDesktopConnector();
|
|
2062
|
+
const result = await connector.lintDesign(nodeId, rules || ['all'], maxDepth || 10, maxFindings || 100);
|
|
2063
|
+
if (!result.success) {
|
|
2064
|
+
throw new Error(result.error || "Lint failed");
|
|
2065
|
+
}
|
|
2066
|
+
return {
|
|
2067
|
+
content: [
|
|
2068
|
+
{
|
|
2069
|
+
type: "text",
|
|
2070
|
+
text: JSON.stringify(result.data || result, null, 2),
|
|
2071
|
+
},
|
|
2072
|
+
],
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
catch (error) {
|
|
2076
|
+
logger.error({ error }, "Failed to lint design");
|
|
2077
|
+
return {
|
|
2078
|
+
content: [
|
|
2079
|
+
{
|
|
2080
|
+
type: "text",
|
|
2081
|
+
text: JSON.stringify({
|
|
2082
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2083
|
+
hint: "Make sure the Desktop Bridge plugin is running in your Figma file.",
|
|
2084
|
+
}),
|
|
2085
|
+
},
|
|
2086
|
+
],
|
|
2087
|
+
isError: true,
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
});
|
|
2091
|
+
}
|