@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,2818 @@
|
|
|
1
|
+
// Figma Desktop Bridge - MCP Plugin
|
|
2
|
+
// Bridges Figma API to MCP clients via plugin UI window
|
|
3
|
+
// Supports: Variables, Components, Styles, and more
|
|
4
|
+
// Uses postMessage to communicate with UI, bypassing worker sandbox limitations
|
|
5
|
+
// Puppeteer can access UI iframe's window context to retrieve data
|
|
6
|
+
|
|
7
|
+
console.log('🌉 [Desktop Bridge] Plugin loaded and ready');
|
|
8
|
+
|
|
9
|
+
// Show minimal UI - compact status indicator
|
|
10
|
+
figma.showUI(__html__, { width: 140, height: 50, visible: true, themeColors: true });
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// CONSOLE CAPTURE — Intercept console.* in the QuickJS sandbox and forward
|
|
14
|
+
// to ui.html via postMessage so the WebSocket bridge can relay them to the MCP
|
|
15
|
+
// server. This enables console monitoring without CDP.
|
|
16
|
+
// ============================================================================
|
|
17
|
+
(function() {
|
|
18
|
+
var levels = ['log', 'info', 'warn', 'error', 'debug'];
|
|
19
|
+
var originals = {};
|
|
20
|
+
for (var i = 0; i < levels.length; i++) {
|
|
21
|
+
originals[levels[i]] = console[levels[i]];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function safeSerialize(val) {
|
|
25
|
+
if (val === null || val === undefined) return val;
|
|
26
|
+
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') return val;
|
|
27
|
+
try {
|
|
28
|
+
// Attempt JSON round-trip for objects/arrays (catches circular refs)
|
|
29
|
+
return JSON.parse(JSON.stringify(val));
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return String(val);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (var i = 0; i < levels.length; i++) {
|
|
36
|
+
(function(level) {
|
|
37
|
+
console[level] = function() {
|
|
38
|
+
// Call the original so output still appears in Figma DevTools
|
|
39
|
+
originals[level].apply(console, arguments);
|
|
40
|
+
|
|
41
|
+
// Serialize arguments safely
|
|
42
|
+
var args = [];
|
|
43
|
+
for (var j = 0; j < arguments.length; j++) {
|
|
44
|
+
args.push(safeSerialize(arguments[j]));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Build message text from all arguments
|
|
48
|
+
var messageParts = [];
|
|
49
|
+
for (var j = 0; j < arguments.length; j++) {
|
|
50
|
+
messageParts.push(typeof arguments[j] === 'string' ? arguments[j] : String(arguments[j]));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
figma.ui.postMessage({
|
|
54
|
+
type: 'CONSOLE_CAPTURE',
|
|
55
|
+
level: level,
|
|
56
|
+
message: messageParts.join(' '),
|
|
57
|
+
args: args,
|
|
58
|
+
timestamp: Date.now()
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
})(levels[i]);
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
|
|
65
|
+
// Immediately fetch and send variables data to UI
|
|
66
|
+
(async () => {
|
|
67
|
+
try {
|
|
68
|
+
console.log('🌉 [Desktop Bridge] Fetching variables...');
|
|
69
|
+
|
|
70
|
+
// Get all local variables and collections
|
|
71
|
+
const variables = await figma.variables.getLocalVariablesAsync();
|
|
72
|
+
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
|
73
|
+
|
|
74
|
+
console.log(`🌉 [Desktop Bridge] Found ${variables.length} variables in ${collections.length} collections`);
|
|
75
|
+
|
|
76
|
+
// Format the data
|
|
77
|
+
const variablesData = {
|
|
78
|
+
success: true,
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
fileKey: figma.fileKey || null,
|
|
81
|
+
variables: variables.map(v => ({
|
|
82
|
+
id: v.id,
|
|
83
|
+
name: v.name,
|
|
84
|
+
key: v.key,
|
|
85
|
+
resolvedType: v.resolvedType,
|
|
86
|
+
valuesByMode: v.valuesByMode,
|
|
87
|
+
variableCollectionId: v.variableCollectionId,
|
|
88
|
+
scopes: v.scopes,
|
|
89
|
+
description: v.description,
|
|
90
|
+
hiddenFromPublishing: v.hiddenFromPublishing
|
|
91
|
+
})),
|
|
92
|
+
variableCollections: collections.map(c => ({
|
|
93
|
+
id: c.id,
|
|
94
|
+
name: c.name,
|
|
95
|
+
key: c.key,
|
|
96
|
+
modes: c.modes,
|
|
97
|
+
defaultModeId: c.defaultModeId,
|
|
98
|
+
variableIds: c.variableIds
|
|
99
|
+
}))
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Send to UI via postMessage
|
|
103
|
+
figma.ui.postMessage({
|
|
104
|
+
type: 'VARIABLES_DATA',
|
|
105
|
+
data: variablesData
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
console.log('🌉 [Desktop Bridge] Variables data sent to UI successfully');
|
|
109
|
+
console.log('🌉 [Desktop Bridge] UI iframe now has variables data accessible via window.__figmaVariablesData');
|
|
110
|
+
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('🌉 [Desktop Bridge] Error fetching variables:', error);
|
|
113
|
+
figma.ui.postMessage({
|
|
114
|
+
type: 'ERROR',
|
|
115
|
+
error: error.message || String(error)
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
|
|
120
|
+
// Helper function to serialize a variable for response
|
|
121
|
+
function serializeVariable(v) {
|
|
122
|
+
return {
|
|
123
|
+
id: v.id,
|
|
124
|
+
name: v.name,
|
|
125
|
+
key: v.key,
|
|
126
|
+
resolvedType: v.resolvedType,
|
|
127
|
+
valuesByMode: v.valuesByMode,
|
|
128
|
+
variableCollectionId: v.variableCollectionId,
|
|
129
|
+
scopes: v.scopes,
|
|
130
|
+
description: v.description,
|
|
131
|
+
hiddenFromPublishing: v.hiddenFromPublishing
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Helper function to serialize a collection for response
|
|
136
|
+
function serializeCollection(c) {
|
|
137
|
+
return {
|
|
138
|
+
id: c.id,
|
|
139
|
+
name: c.name,
|
|
140
|
+
key: c.key,
|
|
141
|
+
modes: c.modes,
|
|
142
|
+
defaultModeId: c.defaultModeId,
|
|
143
|
+
variableIds: c.variableIds
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Helper to convert hex color to Figma RGB (0-1 range)
|
|
148
|
+
function hexToFigmaRGB(hex) {
|
|
149
|
+
// Remove # if present
|
|
150
|
+
hex = hex.replace(/^#/, '');
|
|
151
|
+
|
|
152
|
+
// Validate hex characters BEFORE parsing (prevents NaN values)
|
|
153
|
+
if (!/^[0-9A-Fa-f]+$/.test(hex)) {
|
|
154
|
+
throw new Error('Invalid hex color: "' + hex + '" contains non-hex characters. Use only 0-9 and A-F.');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Parse hex values
|
|
158
|
+
var r, g, b, a = 1;
|
|
159
|
+
|
|
160
|
+
if (hex.length === 3) {
|
|
161
|
+
// #RGB format
|
|
162
|
+
r = parseInt(hex[0] + hex[0], 16) / 255;
|
|
163
|
+
g = parseInt(hex[1] + hex[1], 16) / 255;
|
|
164
|
+
b = parseInt(hex[2] + hex[2], 16) / 255;
|
|
165
|
+
} else if (hex.length === 4) {
|
|
166
|
+
// #RGBA format (CSS4 shorthand)
|
|
167
|
+
r = parseInt(hex[0] + hex[0], 16) / 255;
|
|
168
|
+
g = parseInt(hex[1] + hex[1], 16) / 255;
|
|
169
|
+
b = parseInt(hex[2] + hex[2], 16) / 255;
|
|
170
|
+
a = parseInt(hex[3] + hex[3], 16) / 255;
|
|
171
|
+
} else if (hex.length === 6) {
|
|
172
|
+
// #RRGGBB format
|
|
173
|
+
r = parseInt(hex.substring(0, 2), 16) / 255;
|
|
174
|
+
g = parseInt(hex.substring(2, 4), 16) / 255;
|
|
175
|
+
b = parseInt(hex.substring(4, 6), 16) / 255;
|
|
176
|
+
} else if (hex.length === 8) {
|
|
177
|
+
// #RRGGBBAA format
|
|
178
|
+
r = parseInt(hex.substring(0, 2), 16) / 255;
|
|
179
|
+
g = parseInt(hex.substring(2, 4), 16) / 255;
|
|
180
|
+
b = parseInt(hex.substring(4, 6), 16) / 255;
|
|
181
|
+
a = parseInt(hex.substring(6, 8), 16) / 255;
|
|
182
|
+
} else {
|
|
183
|
+
throw new Error('Invalid hex color format: "' + hex + '". Expected 3, 4, 6, or 8 hex characters (e.g., #RGB, #RGBA, #RRGGBB, #RRGGBBAA).');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { r: r, g: g, b: b, a: a };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Listen for requests from UI (e.g., component data requests, write operations)
|
|
190
|
+
figma.ui.onmessage = async (msg) => {
|
|
191
|
+
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// EXECUTE_CODE - Arbitrary code execution (Power Tool)
|
|
194
|
+
// ============================================================================
|
|
195
|
+
if (msg.type === 'EXECUTE_CODE') {
|
|
196
|
+
try {
|
|
197
|
+
console.log('🌉 [Desktop Bridge] Executing code, length:', msg.code.length);
|
|
198
|
+
|
|
199
|
+
// Use eval with async IIFE wrapper instead of AsyncFunction constructor
|
|
200
|
+
// AsyncFunction is restricted in Figma's plugin sandbox, but eval works
|
|
201
|
+
// See: https://developers.figma.com/docs/plugins/resource-links
|
|
202
|
+
|
|
203
|
+
// Wrap user code in an async IIFE that returns a Promise
|
|
204
|
+
// This allows async/await in user code while using eval
|
|
205
|
+
var wrappedCode = "(async function() {\n" + msg.code + "\n})()";
|
|
206
|
+
|
|
207
|
+
console.log('🌉 [Desktop Bridge] Wrapped code for eval');
|
|
208
|
+
|
|
209
|
+
// Execute with timeout
|
|
210
|
+
var timeoutMs = msg.timeout || 5000;
|
|
211
|
+
var timeoutPromise = new Promise(function(_, reject) {
|
|
212
|
+
setTimeout(function() {
|
|
213
|
+
reject(new Error('Execution timed out after ' + timeoutMs + 'ms'));
|
|
214
|
+
}, timeoutMs);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
var codePromise;
|
|
218
|
+
try {
|
|
219
|
+
// eval returns the Promise from the async IIFE
|
|
220
|
+
codePromise = eval(wrappedCode);
|
|
221
|
+
} catch (syntaxError) {
|
|
222
|
+
// Log the actual syntax error message
|
|
223
|
+
var syntaxErrorMsg = syntaxError && syntaxError.message ? syntaxError.message : String(syntaxError);
|
|
224
|
+
console.error('🌉 [Desktop Bridge] Syntax error in code:', syntaxErrorMsg);
|
|
225
|
+
figma.ui.postMessage({
|
|
226
|
+
type: 'EXECUTE_CODE_RESULT',
|
|
227
|
+
requestId: msg.requestId,
|
|
228
|
+
success: false,
|
|
229
|
+
error: 'Syntax error: ' + syntaxErrorMsg
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
var result = await Promise.race([
|
|
235
|
+
codePromise,
|
|
236
|
+
timeoutPromise
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
console.log('🌉 [Desktop Bridge] Code executed successfully, result type:', typeof result);
|
|
240
|
+
|
|
241
|
+
// Analyze result for potential silent failures
|
|
242
|
+
var resultAnalysis = {
|
|
243
|
+
type: typeof result,
|
|
244
|
+
isNull: result === null,
|
|
245
|
+
isUndefined: result === undefined,
|
|
246
|
+
isEmpty: false,
|
|
247
|
+
warning: null
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Check for empty results that might indicate a failed search/operation
|
|
251
|
+
if (Array.isArray(result)) {
|
|
252
|
+
resultAnalysis.isEmpty = result.length === 0;
|
|
253
|
+
if (resultAnalysis.isEmpty) {
|
|
254
|
+
resultAnalysis.warning = 'Code returned an empty array. If you were searching for nodes, none were found.';
|
|
255
|
+
}
|
|
256
|
+
} else if (result !== null && typeof result === 'object') {
|
|
257
|
+
var keys = Object.keys(result);
|
|
258
|
+
resultAnalysis.isEmpty = keys.length === 0;
|
|
259
|
+
if (resultAnalysis.isEmpty) {
|
|
260
|
+
resultAnalysis.warning = 'Code returned an empty object. The operation may not have found what it was looking for.';
|
|
261
|
+
}
|
|
262
|
+
// Check for common "found nothing" patterns
|
|
263
|
+
if (result.length === 0 || result.count === 0 || result.foundCount === 0 || (result.nodes && result.nodes.length === 0)) {
|
|
264
|
+
resultAnalysis.warning = 'Code returned a result indicating nothing was found (count/length is 0).';
|
|
265
|
+
}
|
|
266
|
+
} else if (result === null) {
|
|
267
|
+
resultAnalysis.warning = 'Code returned null. The requested node or resource may not exist.';
|
|
268
|
+
} else if (result === undefined) {
|
|
269
|
+
resultAnalysis.warning = 'Code returned undefined. Make sure your code has a return statement.';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (resultAnalysis.warning) {
|
|
273
|
+
console.warn('🌉 [Desktop Bridge] ⚠️ Result warning:', resultAnalysis.warning);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
figma.ui.postMessage({
|
|
277
|
+
type: 'EXECUTE_CODE_RESULT',
|
|
278
|
+
requestId: msg.requestId,
|
|
279
|
+
success: true,
|
|
280
|
+
result: result,
|
|
281
|
+
resultAnalysis: resultAnalysis,
|
|
282
|
+
// Include file context so users know which file this executed against
|
|
283
|
+
fileContext: {
|
|
284
|
+
fileName: figma.root.name,
|
|
285
|
+
fileKey: figma.fileKey || null
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
} catch (error) {
|
|
290
|
+
// Extract error message explicitly - don't rely on console.error serialization
|
|
291
|
+
var errorName = error && error.name ? error.name : 'Error';
|
|
292
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
293
|
+
var errorStack = error && error.stack ? error.stack : '';
|
|
294
|
+
|
|
295
|
+
// Log error details as strings so they show up properly in Puppeteer
|
|
296
|
+
console.error('🌉 [Desktop Bridge] Code execution error: [' + errorName + '] ' + errorMsg);
|
|
297
|
+
if (errorStack) {
|
|
298
|
+
console.error('🌉 [Desktop Bridge] Stack:', errorStack);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
figma.ui.postMessage({
|
|
302
|
+
type: 'EXECUTE_CODE_RESULT',
|
|
303
|
+
requestId: msg.requestId,
|
|
304
|
+
success: false,
|
|
305
|
+
error: errorName + ': ' + errorMsg
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// UPDATE_VARIABLE - Update a variable's value in a specific mode
|
|
312
|
+
// ============================================================================
|
|
313
|
+
else if (msg.type === 'UPDATE_VARIABLE') {
|
|
314
|
+
try {
|
|
315
|
+
console.log('🌉 [Desktop Bridge] Updating variable:', msg.variableId);
|
|
316
|
+
|
|
317
|
+
var variable = await figma.variables.getVariableByIdAsync(msg.variableId);
|
|
318
|
+
if (!variable) {
|
|
319
|
+
throw new Error('Variable not found: ' + msg.variableId);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Convert value based on variable type
|
|
323
|
+
var value = msg.value;
|
|
324
|
+
|
|
325
|
+
// Check if value is a variable alias (string starting with "VariableID:")
|
|
326
|
+
if (typeof value === 'string' && value.startsWith('VariableID:')) {
|
|
327
|
+
// Convert to VARIABLE_ALIAS format
|
|
328
|
+
value = {
|
|
329
|
+
type: 'VARIABLE_ALIAS',
|
|
330
|
+
id: value
|
|
331
|
+
};
|
|
332
|
+
console.log('🌉 [Desktop Bridge] Converting to variable alias:', value.id);
|
|
333
|
+
} else if (variable.resolvedType === 'COLOR' && typeof value === 'string') {
|
|
334
|
+
// Convert hex string to Figma color
|
|
335
|
+
value = hexToFigmaRGB(value);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Set the value for the specified mode
|
|
339
|
+
variable.setValueForMode(msg.modeId, value);
|
|
340
|
+
|
|
341
|
+
console.log('🌉 [Desktop Bridge] Variable updated successfully');
|
|
342
|
+
|
|
343
|
+
figma.ui.postMessage({
|
|
344
|
+
type: 'UPDATE_VARIABLE_RESULT',
|
|
345
|
+
requestId: msg.requestId,
|
|
346
|
+
success: true,
|
|
347
|
+
variable: serializeVariable(variable)
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error('🌉 [Desktop Bridge] Update variable error:', error);
|
|
352
|
+
figma.ui.postMessage({
|
|
353
|
+
type: 'UPDATE_VARIABLE_RESULT',
|
|
354
|
+
requestId: msg.requestId,
|
|
355
|
+
success: false,
|
|
356
|
+
error: error.message || String(error)
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ============================================================================
|
|
362
|
+
// CREATE_VARIABLE - Create a new variable in a collection
|
|
363
|
+
// ============================================================================
|
|
364
|
+
else if (msg.type === 'CREATE_VARIABLE') {
|
|
365
|
+
try {
|
|
366
|
+
console.log('🌉 [Desktop Bridge] Creating variable:', msg.name);
|
|
367
|
+
|
|
368
|
+
// Get the collection
|
|
369
|
+
var collection = await figma.variables.getVariableCollectionByIdAsync(msg.collectionId);
|
|
370
|
+
if (!collection) {
|
|
371
|
+
throw new Error('Collection not found: ' + msg.collectionId);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Create the variable
|
|
375
|
+
var variable = figma.variables.createVariable(msg.name, collection, msg.resolvedType);
|
|
376
|
+
|
|
377
|
+
// Set initial values if provided
|
|
378
|
+
if (msg.valuesByMode) {
|
|
379
|
+
for (var modeId in msg.valuesByMode) {
|
|
380
|
+
var value = msg.valuesByMode[modeId];
|
|
381
|
+
// Convert hex colors
|
|
382
|
+
if (msg.resolvedType === 'COLOR' && typeof value === 'string') {
|
|
383
|
+
value = hexToFigmaRGB(value);
|
|
384
|
+
}
|
|
385
|
+
variable.setValueForMode(modeId, value);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Set description if provided
|
|
390
|
+
if (msg.description) {
|
|
391
|
+
variable.description = msg.description;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Set scopes if provided
|
|
395
|
+
if (msg.scopes) {
|
|
396
|
+
variable.scopes = msg.scopes;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
console.log('🌉 [Desktop Bridge] Variable created:', variable.id);
|
|
400
|
+
|
|
401
|
+
figma.ui.postMessage({
|
|
402
|
+
type: 'CREATE_VARIABLE_RESULT',
|
|
403
|
+
requestId: msg.requestId,
|
|
404
|
+
success: true,
|
|
405
|
+
variable: serializeVariable(variable)
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error('🌉 [Desktop Bridge] Create variable error:', error);
|
|
410
|
+
figma.ui.postMessage({
|
|
411
|
+
type: 'CREATE_VARIABLE_RESULT',
|
|
412
|
+
requestId: msg.requestId,
|
|
413
|
+
success: false,
|
|
414
|
+
error: error.message || String(error)
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ============================================================================
|
|
420
|
+
// CREATE_VARIABLE_COLLECTION - Create a new variable collection
|
|
421
|
+
// ============================================================================
|
|
422
|
+
else if (msg.type === 'CREATE_VARIABLE_COLLECTION') {
|
|
423
|
+
try {
|
|
424
|
+
console.log('🌉 [Desktop Bridge] Creating collection:', msg.name);
|
|
425
|
+
|
|
426
|
+
// Create the collection
|
|
427
|
+
var collection = figma.variables.createVariableCollection(msg.name);
|
|
428
|
+
|
|
429
|
+
// Rename the default mode if a name is provided
|
|
430
|
+
if (msg.initialModeName && collection.modes.length > 0) {
|
|
431
|
+
collection.renameMode(collection.modes[0].modeId, msg.initialModeName);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Add additional modes if provided
|
|
435
|
+
if (msg.additionalModes && msg.additionalModes.length > 0) {
|
|
436
|
+
for (var i = 0; i < msg.additionalModes.length; i++) {
|
|
437
|
+
collection.addMode(msg.additionalModes[i]);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
console.log('🌉 [Desktop Bridge] Collection created:', collection.id);
|
|
442
|
+
|
|
443
|
+
figma.ui.postMessage({
|
|
444
|
+
type: 'CREATE_VARIABLE_COLLECTION_RESULT',
|
|
445
|
+
requestId: msg.requestId,
|
|
446
|
+
success: true,
|
|
447
|
+
collection: serializeCollection(collection)
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.error('🌉 [Desktop Bridge] Create collection error:', error);
|
|
452
|
+
figma.ui.postMessage({
|
|
453
|
+
type: 'CREATE_VARIABLE_COLLECTION_RESULT',
|
|
454
|
+
requestId: msg.requestId,
|
|
455
|
+
success: false,
|
|
456
|
+
error: error.message || String(error)
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// DELETE_VARIABLE - Delete a variable
|
|
463
|
+
// ============================================================================
|
|
464
|
+
else if (msg.type === 'DELETE_VARIABLE') {
|
|
465
|
+
try {
|
|
466
|
+
console.log('🌉 [Desktop Bridge] Deleting variable:', msg.variableId);
|
|
467
|
+
|
|
468
|
+
var variable = await figma.variables.getVariableByIdAsync(msg.variableId);
|
|
469
|
+
if (!variable) {
|
|
470
|
+
throw new Error('Variable not found: ' + msg.variableId);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
var deletedInfo = {
|
|
474
|
+
id: variable.id,
|
|
475
|
+
name: variable.name
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
variable.remove();
|
|
479
|
+
|
|
480
|
+
console.log('🌉 [Desktop Bridge] Variable deleted');
|
|
481
|
+
|
|
482
|
+
figma.ui.postMessage({
|
|
483
|
+
type: 'DELETE_VARIABLE_RESULT',
|
|
484
|
+
requestId: msg.requestId,
|
|
485
|
+
success: true,
|
|
486
|
+
deleted: deletedInfo
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
} catch (error) {
|
|
490
|
+
console.error('🌉 [Desktop Bridge] Delete variable error:', error);
|
|
491
|
+
figma.ui.postMessage({
|
|
492
|
+
type: 'DELETE_VARIABLE_RESULT',
|
|
493
|
+
requestId: msg.requestId,
|
|
494
|
+
success: false,
|
|
495
|
+
error: error.message || String(error)
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ============================================================================
|
|
501
|
+
// DELETE_VARIABLE_COLLECTION - Delete a variable collection
|
|
502
|
+
// ============================================================================
|
|
503
|
+
else if (msg.type === 'DELETE_VARIABLE_COLLECTION') {
|
|
504
|
+
try {
|
|
505
|
+
console.log('🌉 [Desktop Bridge] Deleting collection:', msg.collectionId);
|
|
506
|
+
|
|
507
|
+
var collection = await figma.variables.getVariableCollectionByIdAsync(msg.collectionId);
|
|
508
|
+
if (!collection) {
|
|
509
|
+
throw new Error('Collection not found: ' + msg.collectionId);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
var deletedInfo = {
|
|
513
|
+
id: collection.id,
|
|
514
|
+
name: collection.name,
|
|
515
|
+
variableCount: collection.variableIds.length
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
collection.remove();
|
|
519
|
+
|
|
520
|
+
console.log('🌉 [Desktop Bridge] Collection deleted');
|
|
521
|
+
|
|
522
|
+
figma.ui.postMessage({
|
|
523
|
+
type: 'DELETE_VARIABLE_COLLECTION_RESULT',
|
|
524
|
+
requestId: msg.requestId,
|
|
525
|
+
success: true,
|
|
526
|
+
deleted: deletedInfo
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
} catch (error) {
|
|
530
|
+
console.error('🌉 [Desktop Bridge] Delete collection error:', error);
|
|
531
|
+
figma.ui.postMessage({
|
|
532
|
+
type: 'DELETE_VARIABLE_COLLECTION_RESULT',
|
|
533
|
+
requestId: msg.requestId,
|
|
534
|
+
success: false,
|
|
535
|
+
error: error.message || String(error)
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ============================================================================
|
|
541
|
+
// RENAME_VARIABLE - Rename a variable
|
|
542
|
+
// ============================================================================
|
|
543
|
+
else if (msg.type === 'RENAME_VARIABLE') {
|
|
544
|
+
try {
|
|
545
|
+
console.log('🌉 [Desktop Bridge] Renaming variable:', msg.variableId, 'to', msg.newName);
|
|
546
|
+
|
|
547
|
+
var variable = await figma.variables.getVariableByIdAsync(msg.variableId);
|
|
548
|
+
if (!variable) {
|
|
549
|
+
throw new Error('Variable not found: ' + msg.variableId);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
var oldName = variable.name;
|
|
553
|
+
variable.name = msg.newName;
|
|
554
|
+
|
|
555
|
+
console.log('🌉 [Desktop Bridge] Variable renamed from "' + oldName + '" to "' + msg.newName + '"');
|
|
556
|
+
|
|
557
|
+
var serializedVar = serializeVariable(variable);
|
|
558
|
+
serializedVar.oldName = oldName;
|
|
559
|
+
figma.ui.postMessage({
|
|
560
|
+
type: 'RENAME_VARIABLE_RESULT',
|
|
561
|
+
requestId: msg.requestId,
|
|
562
|
+
success: true,
|
|
563
|
+
variable: serializedVar,
|
|
564
|
+
oldName: oldName
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
} catch (error) {
|
|
568
|
+
console.error('🌉 [Desktop Bridge] Rename variable error:', error);
|
|
569
|
+
figma.ui.postMessage({
|
|
570
|
+
type: 'RENAME_VARIABLE_RESULT',
|
|
571
|
+
requestId: msg.requestId,
|
|
572
|
+
success: false,
|
|
573
|
+
error: error.message || String(error)
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ============================================================================
|
|
579
|
+
// SET_VARIABLE_DESCRIPTION - Set description on a variable
|
|
580
|
+
// ============================================================================
|
|
581
|
+
else if (msg.type === 'SET_VARIABLE_DESCRIPTION') {
|
|
582
|
+
try {
|
|
583
|
+
console.log('🌉 [Desktop Bridge] Setting description on variable:', msg.variableId);
|
|
584
|
+
|
|
585
|
+
var variable = await figma.variables.getVariableByIdAsync(msg.variableId);
|
|
586
|
+
if (!variable) {
|
|
587
|
+
throw new Error('Variable not found: ' + msg.variableId);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
variable.description = msg.description || '';
|
|
591
|
+
|
|
592
|
+
console.log('🌉 [Desktop Bridge] Variable description set successfully');
|
|
593
|
+
|
|
594
|
+
figma.ui.postMessage({
|
|
595
|
+
type: 'SET_VARIABLE_DESCRIPTION_RESULT',
|
|
596
|
+
requestId: msg.requestId,
|
|
597
|
+
success: true,
|
|
598
|
+
variable: serializeVariable(variable)
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
} catch (error) {
|
|
602
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
603
|
+
console.error('🌉 [Desktop Bridge] Set variable description error:', errorMsg);
|
|
604
|
+
figma.ui.postMessage({
|
|
605
|
+
type: 'SET_VARIABLE_DESCRIPTION_RESULT',
|
|
606
|
+
requestId: msg.requestId,
|
|
607
|
+
success: false,
|
|
608
|
+
error: errorMsg
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ============================================================================
|
|
614
|
+
// ADD_MODE - Add a mode to a variable collection
|
|
615
|
+
// ============================================================================
|
|
616
|
+
else if (msg.type === 'ADD_MODE') {
|
|
617
|
+
try {
|
|
618
|
+
console.log('🌉 [Desktop Bridge] Adding mode to collection:', msg.collectionId);
|
|
619
|
+
|
|
620
|
+
var collection = await figma.variables.getVariableCollectionByIdAsync(msg.collectionId);
|
|
621
|
+
if (!collection) {
|
|
622
|
+
throw new Error('Collection not found: ' + msg.collectionId);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Add the mode (returns the new mode ID)
|
|
626
|
+
var newModeId = collection.addMode(msg.modeName);
|
|
627
|
+
|
|
628
|
+
console.log('🌉 [Desktop Bridge] Mode "' + msg.modeName + '" added with ID:', newModeId);
|
|
629
|
+
|
|
630
|
+
figma.ui.postMessage({
|
|
631
|
+
type: 'ADD_MODE_RESULT',
|
|
632
|
+
requestId: msg.requestId,
|
|
633
|
+
success: true,
|
|
634
|
+
collection: serializeCollection(collection),
|
|
635
|
+
newMode: {
|
|
636
|
+
modeId: newModeId,
|
|
637
|
+
name: msg.modeName
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
} catch (error) {
|
|
642
|
+
console.error('🌉 [Desktop Bridge] Add mode error:', error);
|
|
643
|
+
figma.ui.postMessage({
|
|
644
|
+
type: 'ADD_MODE_RESULT',
|
|
645
|
+
requestId: msg.requestId,
|
|
646
|
+
success: false,
|
|
647
|
+
error: error.message || String(error)
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ============================================================================
|
|
653
|
+
// RENAME_MODE - Rename a mode in a variable collection
|
|
654
|
+
// ============================================================================
|
|
655
|
+
else if (msg.type === 'RENAME_MODE') {
|
|
656
|
+
try {
|
|
657
|
+
console.log('🌉 [Desktop Bridge] Renaming mode:', msg.modeId, 'in collection:', msg.collectionId);
|
|
658
|
+
|
|
659
|
+
var collection = await figma.variables.getVariableCollectionByIdAsync(msg.collectionId);
|
|
660
|
+
if (!collection) {
|
|
661
|
+
throw new Error('Collection not found: ' + msg.collectionId);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Find the current mode name
|
|
665
|
+
var currentMode = collection.modes.find(function(m) { return m.modeId === msg.modeId; });
|
|
666
|
+
if (!currentMode) {
|
|
667
|
+
throw new Error('Mode not found: ' + msg.modeId);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
var oldName = currentMode.name;
|
|
671
|
+
collection.renameMode(msg.modeId, msg.newName);
|
|
672
|
+
|
|
673
|
+
console.log('🌉 [Desktop Bridge] Mode renamed from "' + oldName + '" to "' + msg.newName + '"');
|
|
674
|
+
|
|
675
|
+
var serializedCol = serializeCollection(collection);
|
|
676
|
+
serializedCol.oldName = oldName;
|
|
677
|
+
figma.ui.postMessage({
|
|
678
|
+
type: 'RENAME_MODE_RESULT',
|
|
679
|
+
requestId: msg.requestId,
|
|
680
|
+
success: true,
|
|
681
|
+
collection: serializedCol,
|
|
682
|
+
oldName: oldName
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
} catch (error) {
|
|
686
|
+
console.error('🌉 [Desktop Bridge] Rename mode error:', error);
|
|
687
|
+
figma.ui.postMessage({
|
|
688
|
+
type: 'RENAME_MODE_RESULT',
|
|
689
|
+
requestId: msg.requestId,
|
|
690
|
+
success: false,
|
|
691
|
+
error: error.message || String(error)
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// ============================================================================
|
|
697
|
+
// REFRESH_VARIABLES - Re-fetch and send all variables data
|
|
698
|
+
// ============================================================================
|
|
699
|
+
else if (msg.type === 'REFRESH_VARIABLES') {
|
|
700
|
+
try {
|
|
701
|
+
console.log('🌉 [Desktop Bridge] Refreshing variables data...');
|
|
702
|
+
|
|
703
|
+
var variables = await figma.variables.getLocalVariablesAsync();
|
|
704
|
+
var collections = await figma.variables.getLocalVariableCollectionsAsync();
|
|
705
|
+
|
|
706
|
+
var variablesData = {
|
|
707
|
+
success: true,
|
|
708
|
+
timestamp: Date.now(),
|
|
709
|
+
fileKey: figma.fileKey || null,
|
|
710
|
+
variables: variables.map(serializeVariable),
|
|
711
|
+
variableCollections: collections.map(serializeCollection)
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// Update the UI's cached data
|
|
715
|
+
figma.ui.postMessage({
|
|
716
|
+
type: 'VARIABLES_DATA',
|
|
717
|
+
data: variablesData
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Also send as a response to the request
|
|
721
|
+
figma.ui.postMessage({
|
|
722
|
+
type: 'REFRESH_VARIABLES_RESULT',
|
|
723
|
+
requestId: msg.requestId,
|
|
724
|
+
success: true,
|
|
725
|
+
data: variablesData
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
console.log('🌉 [Desktop Bridge] Variables refreshed:', variables.length, 'variables in', collections.length, 'collections');
|
|
729
|
+
|
|
730
|
+
} catch (error) {
|
|
731
|
+
console.error('🌉 [Desktop Bridge] Refresh variables error:', error);
|
|
732
|
+
figma.ui.postMessage({
|
|
733
|
+
type: 'REFRESH_VARIABLES_RESULT',
|
|
734
|
+
requestId: msg.requestId,
|
|
735
|
+
success: false,
|
|
736
|
+
error: error.message || String(error)
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ============================================================================
|
|
742
|
+
// GET_COMPONENT - Existing read operation
|
|
743
|
+
// ============================================================================
|
|
744
|
+
else if (msg.type === 'GET_COMPONENT') {
|
|
745
|
+
try {
|
|
746
|
+
console.log(`🌉 [Desktop Bridge] Fetching component: ${msg.nodeId}`);
|
|
747
|
+
|
|
748
|
+
const node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
749
|
+
|
|
750
|
+
if (!node) {
|
|
751
|
+
throw new Error(`Node not found: ${msg.nodeId}`);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET' && node.type !== 'INSTANCE') {
|
|
755
|
+
throw new Error(`Node is not a component. Type: ${node.type}`);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Detect if this is a variant (COMPONENT inside a COMPONENT_SET)
|
|
759
|
+
// Note: Can't use optional chaining (?.) - Figma plugin sandbox doesn't support it
|
|
760
|
+
const isVariant = node.type === 'COMPONENT' && node.parent && node.parent.type === 'COMPONENT_SET';
|
|
761
|
+
|
|
762
|
+
// Extract component data including description fields and annotations
|
|
763
|
+
const componentData = {
|
|
764
|
+
success: true,
|
|
765
|
+
timestamp: Date.now(),
|
|
766
|
+
nodeId: msg.nodeId,
|
|
767
|
+
component: {
|
|
768
|
+
id: node.id,
|
|
769
|
+
name: node.name,
|
|
770
|
+
type: node.type,
|
|
771
|
+
// Variants CAN have their own description
|
|
772
|
+
description: node.description || null,
|
|
773
|
+
descriptionMarkdown: node.descriptionMarkdown || null,
|
|
774
|
+
visible: node.visible,
|
|
775
|
+
locked: node.locked,
|
|
776
|
+
// Dev Mode annotations
|
|
777
|
+
annotations: node.annotations || [],
|
|
778
|
+
// Flag to indicate if this is a variant
|
|
779
|
+
isVariant: isVariant,
|
|
780
|
+
// For component sets and non-variant components only (variants cannot access this)
|
|
781
|
+
componentPropertyDefinitions: (node.type === 'COMPONENT_SET' || (node.type === 'COMPONENT' && !isVariant))
|
|
782
|
+
? node.componentPropertyDefinitions
|
|
783
|
+
: undefined,
|
|
784
|
+
// Get children info (lightweight) — skip unresolvable slot sublayers
|
|
785
|
+
children: node.children ? node.children.reduce((acc, child) => {
|
|
786
|
+
try {
|
|
787
|
+
acc.push({ id: child.id, name: child.name, type: child.type });
|
|
788
|
+
} catch (e) { /* slot sublayer or table cell — skip */ }
|
|
789
|
+
return acc;
|
|
790
|
+
}, []) : undefined
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
console.log(`🌉 [Desktop Bridge] Component data ready. Has description: ${!!componentData.component.description}, annotations: ${componentData.component.annotations.length}`);
|
|
795
|
+
|
|
796
|
+
// Send to UI
|
|
797
|
+
figma.ui.postMessage({
|
|
798
|
+
type: 'COMPONENT_DATA',
|
|
799
|
+
requestId: msg.requestId, // Echo back the request ID
|
|
800
|
+
data: componentData
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
} catch (error) {
|
|
804
|
+
console.error(`🌉 [Desktop Bridge] Error fetching component:`, error);
|
|
805
|
+
figma.ui.postMessage({
|
|
806
|
+
type: 'COMPONENT_ERROR',
|
|
807
|
+
requestId: msg.requestId,
|
|
808
|
+
error: error.message || String(error)
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ============================================================================
|
|
814
|
+
// GET_LOCAL_COMPONENTS - Get all local components for design system manifest
|
|
815
|
+
// ============================================================================
|
|
816
|
+
else if (msg.type === 'GET_LOCAL_COMPONENTS') {
|
|
817
|
+
try {
|
|
818
|
+
console.log('🌉 [Desktop Bridge] Fetching all local components for manifest...');
|
|
819
|
+
|
|
820
|
+
// Find all component sets and standalone components in the file
|
|
821
|
+
var components = [];
|
|
822
|
+
var componentSets = [];
|
|
823
|
+
|
|
824
|
+
// Helper to extract component data
|
|
825
|
+
function extractComponentData(node, isPartOfSet) {
|
|
826
|
+
var data = {
|
|
827
|
+
key: node.key,
|
|
828
|
+
nodeId: node.id,
|
|
829
|
+
name: node.name,
|
|
830
|
+
type: node.type,
|
|
831
|
+
description: node.description || null,
|
|
832
|
+
width: node.width,
|
|
833
|
+
height: node.height
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
// Get property definitions for non-variant components
|
|
837
|
+
if (!isPartOfSet && node.componentPropertyDefinitions) {
|
|
838
|
+
data.properties = [];
|
|
839
|
+
var propDefs = node.componentPropertyDefinitions;
|
|
840
|
+
for (var propName in propDefs) {
|
|
841
|
+
if (propDefs.hasOwnProperty(propName)) {
|
|
842
|
+
var propDef = propDefs[propName];
|
|
843
|
+
data.properties.push({
|
|
844
|
+
name: propName,
|
|
845
|
+
type: propDef.type,
|
|
846
|
+
defaultValue: propDef.defaultValue
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return data;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Helper to extract component set data with all variants
|
|
856
|
+
function extractComponentSetData(node) {
|
|
857
|
+
var variantAxes = {};
|
|
858
|
+
var variants = [];
|
|
859
|
+
|
|
860
|
+
// Parse variant properties from children names — skip unresolvable slot sublayers
|
|
861
|
+
if (node.children) {
|
|
862
|
+
node.children.forEach(function(child) {
|
|
863
|
+
try {
|
|
864
|
+
if (child.type === 'COMPONENT') {
|
|
865
|
+
// Parse variant name (e.g., "Size=md, State=default")
|
|
866
|
+
var variantProps = {};
|
|
867
|
+
var parts = child.name.split(',').map(function(p) { return p.trim(); });
|
|
868
|
+
parts.forEach(function(part) {
|
|
869
|
+
var kv = part.split('=');
|
|
870
|
+
if (kv.length === 2) {
|
|
871
|
+
var key = kv[0].trim();
|
|
872
|
+
var value = kv[1].trim();
|
|
873
|
+
variantProps[key] = value;
|
|
874
|
+
|
|
875
|
+
// Track all values for each axis
|
|
876
|
+
if (!variantAxes[key]) {
|
|
877
|
+
variantAxes[key] = [];
|
|
878
|
+
}
|
|
879
|
+
if (variantAxes[key].indexOf(value) === -1) {
|
|
880
|
+
variantAxes[key].push(value);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
variants.push({
|
|
886
|
+
key: child.key,
|
|
887
|
+
nodeId: child.id,
|
|
888
|
+
name: child.name,
|
|
889
|
+
description: child.description || null,
|
|
890
|
+
variantProperties: variantProps,
|
|
891
|
+
width: child.width,
|
|
892
|
+
height: child.height
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
} catch (e) { /* slot sublayer — skip */ }
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Convert variantAxes object to array format
|
|
900
|
+
var axes = [];
|
|
901
|
+
for (var axisName in variantAxes) {
|
|
902
|
+
if (variantAxes.hasOwnProperty(axisName)) {
|
|
903
|
+
axes.push({
|
|
904
|
+
name: axisName,
|
|
905
|
+
values: variantAxes[axisName]
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return {
|
|
911
|
+
key: node.key,
|
|
912
|
+
nodeId: node.id,
|
|
913
|
+
name: node.name,
|
|
914
|
+
type: 'COMPONENT_SET',
|
|
915
|
+
description: node.description || null,
|
|
916
|
+
variantAxes: axes,
|
|
917
|
+
variants: variants,
|
|
918
|
+
defaultVariant: variants.length > 0 ? variants[0] : null,
|
|
919
|
+
properties: node.componentPropertyDefinitions ? Object.keys(node.componentPropertyDefinitions).map(function(propName) {
|
|
920
|
+
var propDef = node.componentPropertyDefinitions[propName];
|
|
921
|
+
return {
|
|
922
|
+
name: propName,
|
|
923
|
+
type: propDef.type,
|
|
924
|
+
defaultValue: propDef.defaultValue
|
|
925
|
+
};
|
|
926
|
+
}) : []
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Recursively search for components
|
|
931
|
+
function findComponents(node) {
|
|
932
|
+
if (!node) return;
|
|
933
|
+
|
|
934
|
+
if (node.type === 'COMPONENT_SET') {
|
|
935
|
+
componentSets.push(extractComponentSetData(node));
|
|
936
|
+
} else if (node.type === 'COMPONENT') {
|
|
937
|
+
// Only add standalone components (not variants inside component sets)
|
|
938
|
+
if (!node.parent || node.parent.type !== 'COMPONENT_SET') {
|
|
939
|
+
components.push(extractComponentData(node, false));
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Recurse into children — skip unresolvable slot sublayers
|
|
944
|
+
if (node.children) {
|
|
945
|
+
node.children.forEach(function(child) {
|
|
946
|
+
try { findComponents(child); } catch (e) { /* slot sublayer — skip */ }
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Load all pages first (required before accessing children)
|
|
952
|
+
console.log('🌉 [Desktop Bridge] Loading all pages...');
|
|
953
|
+
await figma.loadAllPagesAsync();
|
|
954
|
+
|
|
955
|
+
// Process pages in batches with event loop yields to prevent UI freeze
|
|
956
|
+
// This is critical for large design systems that could otherwise crash
|
|
957
|
+
var pages = figma.root.children;
|
|
958
|
+
var PAGE_BATCH_SIZE = 3; // Process 3 pages at a time
|
|
959
|
+
var totalPages = pages.length;
|
|
960
|
+
|
|
961
|
+
console.log('🌉 [Desktop Bridge] Processing ' + totalPages + ' pages in batches of ' + PAGE_BATCH_SIZE + '...');
|
|
962
|
+
|
|
963
|
+
for (var pageIndex = 0; pageIndex < totalPages; pageIndex += PAGE_BATCH_SIZE) {
|
|
964
|
+
var batchEnd = Math.min(pageIndex + PAGE_BATCH_SIZE, totalPages);
|
|
965
|
+
var batchPages = [];
|
|
966
|
+
for (var j = pageIndex; j < batchEnd; j++) {
|
|
967
|
+
batchPages.push(pages[j]);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Process this batch of pages
|
|
971
|
+
batchPages.forEach(function(page) {
|
|
972
|
+
findComponents(page);
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
// Log progress for large files
|
|
976
|
+
if (totalPages > PAGE_BATCH_SIZE) {
|
|
977
|
+
console.log('🌉 [Desktop Bridge] Processed pages ' + (pageIndex + 1) + '-' + batchEnd + ' of ' + totalPages + ' (found ' + components.length + ' components so far)');
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Yield to event loop between batches to prevent UI freeze and allow cancellation
|
|
981
|
+
if (batchEnd < totalPages) {
|
|
982
|
+
await new Promise(function(resolve) { setTimeout(resolve, 0); });
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
console.log('🌉 [Desktop Bridge] Found ' + components.length + ' components and ' + componentSets.length + ' component sets');
|
|
987
|
+
|
|
988
|
+
figma.ui.postMessage({
|
|
989
|
+
type: 'GET_LOCAL_COMPONENTS_RESULT',
|
|
990
|
+
requestId: msg.requestId,
|
|
991
|
+
success: true,
|
|
992
|
+
data: {
|
|
993
|
+
components: components,
|
|
994
|
+
componentSets: componentSets,
|
|
995
|
+
totalComponents: components.length,
|
|
996
|
+
totalComponentSets: componentSets.length,
|
|
997
|
+
// Include file metadata for context verification
|
|
998
|
+
fileName: figma.root.name,
|
|
999
|
+
fileKey: figma.fileKey || null,
|
|
1000
|
+
timestamp: Date.now()
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1006
|
+
console.error('🌉 [Desktop Bridge] Get local components error:', errorMsg);
|
|
1007
|
+
figma.ui.postMessage({
|
|
1008
|
+
type: 'GET_LOCAL_COMPONENTS_RESULT',
|
|
1009
|
+
requestId: msg.requestId,
|
|
1010
|
+
success: false,
|
|
1011
|
+
error: errorMsg
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// ============================================================================
|
|
1017
|
+
// INSTANTIATE_COMPONENT - Create a component instance with overrides
|
|
1018
|
+
// ============================================================================
|
|
1019
|
+
else if (msg.type === 'INSTANTIATE_COMPONENT') {
|
|
1020
|
+
try {
|
|
1021
|
+
console.log('🌉 [Desktop Bridge] Instantiating component:', msg.componentKey || msg.nodeId);
|
|
1022
|
+
|
|
1023
|
+
var component = null;
|
|
1024
|
+
var instance = null;
|
|
1025
|
+
|
|
1026
|
+
// Try published library first (by key), then fall back to local component (by nodeId)
|
|
1027
|
+
if (msg.componentKey) {
|
|
1028
|
+
try {
|
|
1029
|
+
component = await figma.importComponentByKeyAsync(msg.componentKey);
|
|
1030
|
+
} catch (importError) {
|
|
1031
|
+
console.log('🌉 [Desktop Bridge] Not a published component, trying local...');
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Fall back to local component by nodeId
|
|
1036
|
+
if (!component && msg.nodeId) {
|
|
1037
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1038
|
+
if (node) {
|
|
1039
|
+
if (node.type === 'COMPONENT') {
|
|
1040
|
+
component = node;
|
|
1041
|
+
} else if (node.type === 'COMPONENT_SET') {
|
|
1042
|
+
// For component sets, find the right variant or use default
|
|
1043
|
+
if (msg.variant && node.children && node.children.length > 0) {
|
|
1044
|
+
// Build variant name from properties (e.g., "Type=Simple, State=Default")
|
|
1045
|
+
var variantParts = [];
|
|
1046
|
+
for (var prop in msg.variant) {
|
|
1047
|
+
if (msg.variant.hasOwnProperty(prop)) {
|
|
1048
|
+
variantParts.push(prop + '=' + msg.variant[prop]);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
var targetVariantName = variantParts.join(', ');
|
|
1052
|
+
console.log('🌉 [Desktop Bridge] Looking for variant:', targetVariantName);
|
|
1053
|
+
|
|
1054
|
+
// Find matching variant
|
|
1055
|
+
for (var i = 0; i < node.children.length; i++) {
|
|
1056
|
+
var child = node.children[i];
|
|
1057
|
+
if (child.type === 'COMPONENT' && child.name === targetVariantName) {
|
|
1058
|
+
component = child;
|
|
1059
|
+
console.log('🌉 [Desktop Bridge] Found exact variant match');
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// If no exact match, try partial match
|
|
1065
|
+
if (!component) {
|
|
1066
|
+
for (var i = 0; i < node.children.length; i++) {
|
|
1067
|
+
var child = node.children[i];
|
|
1068
|
+
if (child.type === 'COMPONENT') {
|
|
1069
|
+
var matches = true;
|
|
1070
|
+
for (var prop in msg.variant) {
|
|
1071
|
+
if (msg.variant.hasOwnProperty(prop)) {
|
|
1072
|
+
var expected = prop + '=' + msg.variant[prop];
|
|
1073
|
+
if (child.name.indexOf(expected) === -1) {
|
|
1074
|
+
matches = false;
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (matches) {
|
|
1080
|
+
component = child;
|
|
1081
|
+
console.log('🌉 [Desktop Bridge] Found partial variant match:', child.name);
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Default to first variant if no match
|
|
1090
|
+
if (!component && node.children && node.children.length > 0) {
|
|
1091
|
+
component = node.children[0];
|
|
1092
|
+
console.log('🌉 [Desktop Bridge] Using default variant:', component.name);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (!component) {
|
|
1099
|
+
// Build detailed error message with actionable guidance
|
|
1100
|
+
var errorParts = ['Component not found.'];
|
|
1101
|
+
|
|
1102
|
+
if (msg.componentKey && !msg.nodeId) {
|
|
1103
|
+
errorParts.push('Component key "' + msg.componentKey + '" not found. Note: componentKey only works for components from published libraries. For local/unpublished components, you must provide nodeId instead.');
|
|
1104
|
+
} else if (msg.componentKey && msg.nodeId) {
|
|
1105
|
+
errorParts.push('Neither componentKey "' + msg.componentKey + '" nor nodeId "' + msg.nodeId + '" resolved to a valid component. The identifiers may be stale from a previous session.');
|
|
1106
|
+
} else if (msg.nodeId) {
|
|
1107
|
+
errorParts.push('NodeId "' + msg.nodeId + '" does not exist in this file. NodeIds are session-specific and become stale when Figma restarts or the file is closed.');
|
|
1108
|
+
} else {
|
|
1109
|
+
errorParts.push('No componentKey or nodeId was provided.');
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
errorParts.push('SOLUTION: Call figma_search_components to get fresh identifiers, then pass BOTH componentKey AND nodeId together for reliable instantiation.');
|
|
1113
|
+
|
|
1114
|
+
throw new Error(errorParts.join(' '));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Create the instance
|
|
1118
|
+
instance = component.createInstance();
|
|
1119
|
+
|
|
1120
|
+
// Apply position if specified
|
|
1121
|
+
if (msg.position) {
|
|
1122
|
+
instance.x = msg.position.x || 0;
|
|
1123
|
+
instance.y = msg.position.y || 0;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// Apply size override if specified
|
|
1127
|
+
if (msg.size) {
|
|
1128
|
+
instance.resize(msg.size.width, msg.size.height);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Apply property overrides
|
|
1132
|
+
if (msg.overrides) {
|
|
1133
|
+
for (var propName in msg.overrides) {
|
|
1134
|
+
if (msg.overrides.hasOwnProperty(propName)) {
|
|
1135
|
+
try {
|
|
1136
|
+
instance.setProperties({ [propName]: msg.overrides[propName] });
|
|
1137
|
+
} catch (propError) {
|
|
1138
|
+
console.warn('🌉 [Desktop Bridge] Could not set property ' + propName + ':', propError.message);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Apply variant selection if specified
|
|
1145
|
+
if (msg.variant) {
|
|
1146
|
+
try {
|
|
1147
|
+
instance.setProperties(msg.variant);
|
|
1148
|
+
} catch (variantError) {
|
|
1149
|
+
console.warn('🌉 [Desktop Bridge] Could not set variant:', variantError.message);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Append to parent if specified
|
|
1154
|
+
if (msg.parentId) {
|
|
1155
|
+
var parent = await figma.getNodeByIdAsync(msg.parentId);
|
|
1156
|
+
if (parent && 'appendChild' in parent) {
|
|
1157
|
+
parent.appendChild(instance);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
console.log('🌉 [Desktop Bridge] Component instantiated:', instance.id);
|
|
1162
|
+
|
|
1163
|
+
figma.ui.postMessage({
|
|
1164
|
+
type: 'INSTANTIATE_COMPONENT_RESULT',
|
|
1165
|
+
requestId: msg.requestId,
|
|
1166
|
+
success: true,
|
|
1167
|
+
instance: {
|
|
1168
|
+
id: instance.id,
|
|
1169
|
+
name: instance.name,
|
|
1170
|
+
x: instance.x,
|
|
1171
|
+
y: instance.y,
|
|
1172
|
+
width: instance.width,
|
|
1173
|
+
height: instance.height
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1179
|
+
console.error('🌉 [Desktop Bridge] Instantiate component error:', errorMsg);
|
|
1180
|
+
figma.ui.postMessage({
|
|
1181
|
+
type: 'INSTANTIATE_COMPONENT_RESULT',
|
|
1182
|
+
requestId: msg.requestId,
|
|
1183
|
+
success: false,
|
|
1184
|
+
error: errorMsg
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// ============================================================================
|
|
1190
|
+
// SET_NODE_DESCRIPTION - Set description on component/style
|
|
1191
|
+
// ============================================================================
|
|
1192
|
+
else if (msg.type === 'SET_NODE_DESCRIPTION') {
|
|
1193
|
+
try {
|
|
1194
|
+
console.log('🌉 [Desktop Bridge] Setting description on node:', msg.nodeId);
|
|
1195
|
+
|
|
1196
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1197
|
+
if (!node) {
|
|
1198
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Check if node supports description
|
|
1202
|
+
if (!('description' in node)) {
|
|
1203
|
+
throw new Error('Node type ' + node.type + ' does not support description');
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Set description (and markdown if supported)
|
|
1207
|
+
node.description = msg.description || '';
|
|
1208
|
+
if (msg.descriptionMarkdown && 'descriptionMarkdown' in node) {
|
|
1209
|
+
node.descriptionMarkdown = msg.descriptionMarkdown;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
console.log('🌉 [Desktop Bridge] Description set successfully');
|
|
1213
|
+
|
|
1214
|
+
figma.ui.postMessage({
|
|
1215
|
+
type: 'SET_NODE_DESCRIPTION_RESULT',
|
|
1216
|
+
requestId: msg.requestId,
|
|
1217
|
+
success: true,
|
|
1218
|
+
node: { id: node.id, name: node.name, description: node.description }
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1223
|
+
console.error('🌉 [Desktop Bridge] Set description error:', errorMsg);
|
|
1224
|
+
figma.ui.postMessage({
|
|
1225
|
+
type: 'SET_NODE_DESCRIPTION_RESULT',
|
|
1226
|
+
requestId: msg.requestId,
|
|
1227
|
+
success: false,
|
|
1228
|
+
error: errorMsg
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// ============================================================================
|
|
1234
|
+
// ADD_COMPONENT_PROPERTY - Add property to component
|
|
1235
|
+
// ============================================================================
|
|
1236
|
+
else if (msg.type === 'ADD_COMPONENT_PROPERTY') {
|
|
1237
|
+
try {
|
|
1238
|
+
console.log('🌉 [Desktop Bridge] Adding component property:', msg.propertyName, 'type:', msg.propertyType);
|
|
1239
|
+
|
|
1240
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1241
|
+
if (!node) {
|
|
1242
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET') {
|
|
1246
|
+
throw new Error('Node must be a COMPONENT or COMPONENT_SET. Got: ' + node.type);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Check if it's a variant (can't add properties to variants)
|
|
1250
|
+
if (node.type === 'COMPONENT' && node.parent && node.parent.type === 'COMPONENT_SET') {
|
|
1251
|
+
throw new Error('Cannot add properties to variant components. Add to the parent COMPONENT_SET instead.');
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Build options if preferredValues provided
|
|
1255
|
+
var options = undefined;
|
|
1256
|
+
if (msg.preferredValues) {
|
|
1257
|
+
options = { preferredValues: msg.preferredValues };
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Use msg.propertyType (not msg.type which is the message type 'ADD_COMPONENT_PROPERTY')
|
|
1261
|
+
var propertyNameWithId = node.addComponentProperty(msg.propertyName, msg.propertyType, msg.defaultValue, options);
|
|
1262
|
+
|
|
1263
|
+
console.log('🌉 [Desktop Bridge] Property added:', propertyNameWithId);
|
|
1264
|
+
|
|
1265
|
+
figma.ui.postMessage({
|
|
1266
|
+
type: 'ADD_COMPONENT_PROPERTY_RESULT',
|
|
1267
|
+
requestId: msg.requestId,
|
|
1268
|
+
success: true,
|
|
1269
|
+
propertyName: propertyNameWithId
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1274
|
+
console.error('🌉 [Desktop Bridge] Add component property error:', errorMsg);
|
|
1275
|
+
figma.ui.postMessage({
|
|
1276
|
+
type: 'ADD_COMPONENT_PROPERTY_RESULT',
|
|
1277
|
+
requestId: msg.requestId,
|
|
1278
|
+
success: false,
|
|
1279
|
+
error: errorMsg
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// ============================================================================
|
|
1285
|
+
// EDIT_COMPONENT_PROPERTY - Edit existing component property
|
|
1286
|
+
// ============================================================================
|
|
1287
|
+
else if (msg.type === 'EDIT_COMPONENT_PROPERTY') {
|
|
1288
|
+
try {
|
|
1289
|
+
console.log('🌉 [Desktop Bridge] Editing component property:', msg.propertyName);
|
|
1290
|
+
|
|
1291
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1292
|
+
if (!node) {
|
|
1293
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET') {
|
|
1297
|
+
throw new Error('Node must be a COMPONENT or COMPONENT_SET. Got: ' + node.type);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
var propertyNameWithId = node.editComponentProperty(msg.propertyName, msg.newValue);
|
|
1301
|
+
|
|
1302
|
+
console.log('🌉 [Desktop Bridge] Property edited:', propertyNameWithId);
|
|
1303
|
+
|
|
1304
|
+
figma.ui.postMessage({
|
|
1305
|
+
type: 'EDIT_COMPONENT_PROPERTY_RESULT',
|
|
1306
|
+
requestId: msg.requestId,
|
|
1307
|
+
success: true,
|
|
1308
|
+
propertyName: propertyNameWithId
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1313
|
+
console.error('🌉 [Desktop Bridge] Edit component property error:', errorMsg);
|
|
1314
|
+
figma.ui.postMessage({
|
|
1315
|
+
type: 'EDIT_COMPONENT_PROPERTY_RESULT',
|
|
1316
|
+
requestId: msg.requestId,
|
|
1317
|
+
success: false,
|
|
1318
|
+
error: errorMsg
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// ============================================================================
|
|
1324
|
+
// DELETE_COMPONENT_PROPERTY - Delete a component property
|
|
1325
|
+
// ============================================================================
|
|
1326
|
+
else if (msg.type === 'DELETE_COMPONENT_PROPERTY') {
|
|
1327
|
+
try {
|
|
1328
|
+
console.log('🌉 [Desktop Bridge] Deleting component property:', msg.propertyName);
|
|
1329
|
+
|
|
1330
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1331
|
+
if (!node) {
|
|
1332
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET') {
|
|
1336
|
+
throw new Error('Node must be a COMPONENT or COMPONENT_SET. Got: ' + node.type);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
node.deleteComponentProperty(msg.propertyName);
|
|
1340
|
+
|
|
1341
|
+
console.log('🌉 [Desktop Bridge] Property deleted');
|
|
1342
|
+
|
|
1343
|
+
figma.ui.postMessage({
|
|
1344
|
+
type: 'DELETE_COMPONENT_PROPERTY_RESULT',
|
|
1345
|
+
requestId: msg.requestId,
|
|
1346
|
+
success: true
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1351
|
+
console.error('🌉 [Desktop Bridge] Delete component property error:', errorMsg);
|
|
1352
|
+
figma.ui.postMessage({
|
|
1353
|
+
type: 'DELETE_COMPONENT_PROPERTY_RESULT',
|
|
1354
|
+
requestId: msg.requestId,
|
|
1355
|
+
success: false,
|
|
1356
|
+
error: errorMsg
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// ============================================================================
|
|
1362
|
+
// RESIZE_NODE - Resize any node
|
|
1363
|
+
// ============================================================================
|
|
1364
|
+
else if (msg.type === 'RESIZE_NODE') {
|
|
1365
|
+
try {
|
|
1366
|
+
console.log('🌉 [Desktop Bridge] Resizing node:', msg.nodeId);
|
|
1367
|
+
|
|
1368
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1369
|
+
if (!node) {
|
|
1370
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if (!('resize' in node)) {
|
|
1374
|
+
throw new Error('Node type ' + node.type + ' does not support resize');
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
if (msg.withConstraints) {
|
|
1378
|
+
node.resize(msg.width, msg.height);
|
|
1379
|
+
} else {
|
|
1380
|
+
node.resizeWithoutConstraints(msg.width, msg.height);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
console.log('🌉 [Desktop Bridge] Node resized to:', msg.width, 'x', msg.height);
|
|
1384
|
+
|
|
1385
|
+
figma.ui.postMessage({
|
|
1386
|
+
type: 'RESIZE_NODE_RESULT',
|
|
1387
|
+
requestId: msg.requestId,
|
|
1388
|
+
success: true,
|
|
1389
|
+
node: { id: node.id, name: node.name, width: node.width, height: node.height }
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1394
|
+
console.error('🌉 [Desktop Bridge] Resize node error:', errorMsg);
|
|
1395
|
+
figma.ui.postMessage({
|
|
1396
|
+
type: 'RESIZE_NODE_RESULT',
|
|
1397
|
+
requestId: msg.requestId,
|
|
1398
|
+
success: false,
|
|
1399
|
+
error: errorMsg
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// ============================================================================
|
|
1405
|
+
// MOVE_NODE - Move/position a node
|
|
1406
|
+
// ============================================================================
|
|
1407
|
+
else if (msg.type === 'MOVE_NODE') {
|
|
1408
|
+
try {
|
|
1409
|
+
console.log('🌉 [Desktop Bridge] Moving node:', msg.nodeId);
|
|
1410
|
+
|
|
1411
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1412
|
+
if (!node) {
|
|
1413
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
if (!('x' in node)) {
|
|
1417
|
+
throw new Error('Node type ' + node.type + ' does not support positioning');
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
node.x = msg.x;
|
|
1421
|
+
node.y = msg.y;
|
|
1422
|
+
|
|
1423
|
+
console.log('🌉 [Desktop Bridge] Node moved to:', msg.x, ',', msg.y);
|
|
1424
|
+
|
|
1425
|
+
figma.ui.postMessage({
|
|
1426
|
+
type: 'MOVE_NODE_RESULT',
|
|
1427
|
+
requestId: msg.requestId,
|
|
1428
|
+
success: true,
|
|
1429
|
+
node: { id: node.id, name: node.name, x: node.x, y: node.y }
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1434
|
+
console.error('🌉 [Desktop Bridge] Move node error:', errorMsg);
|
|
1435
|
+
figma.ui.postMessage({
|
|
1436
|
+
type: 'MOVE_NODE_RESULT',
|
|
1437
|
+
requestId: msg.requestId,
|
|
1438
|
+
success: false,
|
|
1439
|
+
error: errorMsg
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// ============================================================================
|
|
1445
|
+
// SET_NODE_FILLS - Set fills (colors) on a node
|
|
1446
|
+
// ============================================================================
|
|
1447
|
+
else if (msg.type === 'SET_NODE_FILLS') {
|
|
1448
|
+
try {
|
|
1449
|
+
console.log('🌉 [Desktop Bridge] Setting fills on node:', msg.nodeId);
|
|
1450
|
+
|
|
1451
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1452
|
+
if (!node) {
|
|
1453
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (!('fills' in node)) {
|
|
1457
|
+
throw new Error('Node type ' + node.type + ' does not support fills');
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// Process fills - convert hex colors if needed
|
|
1461
|
+
var processedFills = msg.fills.map(function(fill) {
|
|
1462
|
+
if (fill.type === 'SOLID' && typeof fill.color === 'string') {
|
|
1463
|
+
// Convert hex to RGB
|
|
1464
|
+
var rgb = hexToFigmaRGB(fill.color);
|
|
1465
|
+
return {
|
|
1466
|
+
type: 'SOLID',
|
|
1467
|
+
color: { r: rgb.r, g: rgb.g, b: rgb.b },
|
|
1468
|
+
opacity: rgb.a !== undefined ? rgb.a : (fill.opacity !== undefined ? fill.opacity : 1)
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
return fill;
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
node.fills = processedFills;
|
|
1475
|
+
|
|
1476
|
+
console.log('🌉 [Desktop Bridge] Fills set successfully');
|
|
1477
|
+
|
|
1478
|
+
figma.ui.postMessage({
|
|
1479
|
+
type: 'SET_NODE_FILLS_RESULT',
|
|
1480
|
+
requestId: msg.requestId,
|
|
1481
|
+
success: true,
|
|
1482
|
+
node: { id: node.id, name: node.name }
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1487
|
+
console.error('🌉 [Desktop Bridge] Set fills error:', errorMsg);
|
|
1488
|
+
figma.ui.postMessage({
|
|
1489
|
+
type: 'SET_NODE_FILLS_RESULT',
|
|
1490
|
+
requestId: msg.requestId,
|
|
1491
|
+
success: false,
|
|
1492
|
+
error: errorMsg
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// ============================================================================
|
|
1498
|
+
// SET_IMAGE_FILL - Set an image fill on one or more nodes
|
|
1499
|
+
// Receives raw image bytes (as Array) from ui.html which decodes base64
|
|
1500
|
+
// ============================================================================
|
|
1501
|
+
else if (msg.type === 'SET_IMAGE_FILL') {
|
|
1502
|
+
try {
|
|
1503
|
+
console.log('🌉 [Desktop Bridge] Setting image fill, bytes:', msg.imageBytes.length);
|
|
1504
|
+
|
|
1505
|
+
// Convert the plain array back to Uint8Array
|
|
1506
|
+
var bytes = new Uint8Array(msg.imageBytes);
|
|
1507
|
+
|
|
1508
|
+
// Create the image in Figma
|
|
1509
|
+
var image = figma.createImage(bytes);
|
|
1510
|
+
var imageHash = image.hash;
|
|
1511
|
+
|
|
1512
|
+
var fill = {
|
|
1513
|
+
type: 'IMAGE',
|
|
1514
|
+
scaleMode: msg.scaleMode || 'FILL',
|
|
1515
|
+
imageHash: imageHash
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
// Resolve target nodes
|
|
1519
|
+
var nodeIds = msg.nodeIds || (msg.nodeId ? [msg.nodeId] : []);
|
|
1520
|
+
var updatedCount = 0;
|
|
1521
|
+
var updatedNodes = [];
|
|
1522
|
+
|
|
1523
|
+
for (var i = 0; i < nodeIds.length; i++) {
|
|
1524
|
+
var node = await figma.getNodeByIdAsync(nodeIds[i]);
|
|
1525
|
+
if (node && 'fills' in node) {
|
|
1526
|
+
node.fills = [fill];
|
|
1527
|
+
updatedCount++;
|
|
1528
|
+
updatedNodes.push({ id: node.id, name: node.name });
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
console.log('🌉 [Desktop Bridge] Image fill applied to', updatedCount, 'node(s), hash:', imageHash);
|
|
1533
|
+
|
|
1534
|
+
figma.ui.postMessage({
|
|
1535
|
+
type: 'SET_IMAGE_FILL_RESULT',
|
|
1536
|
+
requestId: msg.requestId,
|
|
1537
|
+
success: true,
|
|
1538
|
+
imageHash: imageHash,
|
|
1539
|
+
updatedCount: updatedCount,
|
|
1540
|
+
nodes: updatedNodes
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1545
|
+
console.error('🌉 [Desktop Bridge] Set image fill error:', errorMsg);
|
|
1546
|
+
figma.ui.postMessage({
|
|
1547
|
+
type: 'SET_IMAGE_FILL_RESULT',
|
|
1548
|
+
requestId: msg.requestId,
|
|
1549
|
+
success: false,
|
|
1550
|
+
error: errorMsg
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// ============================================================================
|
|
1556
|
+
// SET_NODE_STROKES - Set strokes on a node
|
|
1557
|
+
// ============================================================================
|
|
1558
|
+
else if (msg.type === 'SET_NODE_STROKES') {
|
|
1559
|
+
try {
|
|
1560
|
+
console.log('🌉 [Desktop Bridge] Setting strokes on node:', msg.nodeId);
|
|
1561
|
+
|
|
1562
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1563
|
+
if (!node) {
|
|
1564
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
if (!('strokes' in node)) {
|
|
1568
|
+
throw new Error('Node type ' + node.type + ' does not support strokes');
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Process strokes - convert hex colors if needed
|
|
1572
|
+
var processedStrokes = msg.strokes.map(function(stroke) {
|
|
1573
|
+
if (stroke.type === 'SOLID' && typeof stroke.color === 'string') {
|
|
1574
|
+
var rgb = hexToFigmaRGB(stroke.color);
|
|
1575
|
+
return {
|
|
1576
|
+
type: 'SOLID',
|
|
1577
|
+
color: { r: rgb.r, g: rgb.g, b: rgb.b },
|
|
1578
|
+
opacity: rgb.a !== undefined ? rgb.a : (stroke.opacity !== undefined ? stroke.opacity : 1)
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
return stroke;
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
node.strokes = processedStrokes;
|
|
1585
|
+
|
|
1586
|
+
if (msg.strokeWeight !== undefined) {
|
|
1587
|
+
node.strokeWeight = msg.strokeWeight;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
console.log('🌉 [Desktop Bridge] Strokes set successfully');
|
|
1591
|
+
|
|
1592
|
+
figma.ui.postMessage({
|
|
1593
|
+
type: 'SET_NODE_STROKES_RESULT',
|
|
1594
|
+
requestId: msg.requestId,
|
|
1595
|
+
success: true,
|
|
1596
|
+
node: { id: node.id, name: node.name }
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
} catch (error) {
|
|
1600
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1601
|
+
console.error('🌉 [Desktop Bridge] Set strokes error:', errorMsg);
|
|
1602
|
+
figma.ui.postMessage({
|
|
1603
|
+
type: 'SET_NODE_STROKES_RESULT',
|
|
1604
|
+
requestId: msg.requestId,
|
|
1605
|
+
success: false,
|
|
1606
|
+
error: errorMsg
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// ============================================================================
|
|
1612
|
+
// SET_NODE_OPACITY - Set opacity on a node
|
|
1613
|
+
// ============================================================================
|
|
1614
|
+
else if (msg.type === 'SET_NODE_OPACITY') {
|
|
1615
|
+
try {
|
|
1616
|
+
console.log('🌉 [Desktop Bridge] Setting opacity on node:', msg.nodeId);
|
|
1617
|
+
|
|
1618
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1619
|
+
if (!node) {
|
|
1620
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
if (!('opacity' in node)) {
|
|
1624
|
+
throw new Error('Node type ' + node.type + ' does not support opacity');
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
node.opacity = Math.max(0, Math.min(1, msg.opacity));
|
|
1628
|
+
|
|
1629
|
+
console.log('🌉 [Desktop Bridge] Opacity set to:', node.opacity);
|
|
1630
|
+
|
|
1631
|
+
figma.ui.postMessage({
|
|
1632
|
+
type: 'SET_NODE_OPACITY_RESULT',
|
|
1633
|
+
requestId: msg.requestId,
|
|
1634
|
+
success: true,
|
|
1635
|
+
node: { id: node.id, name: node.name, opacity: node.opacity }
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1640
|
+
console.error('🌉 [Desktop Bridge] Set opacity error:', errorMsg);
|
|
1641
|
+
figma.ui.postMessage({
|
|
1642
|
+
type: 'SET_NODE_OPACITY_RESULT',
|
|
1643
|
+
requestId: msg.requestId,
|
|
1644
|
+
success: false,
|
|
1645
|
+
error: errorMsg
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// ============================================================================
|
|
1651
|
+
// SET_NODE_CORNER_RADIUS - Set corner radius on a node
|
|
1652
|
+
// ============================================================================
|
|
1653
|
+
else if (msg.type === 'SET_NODE_CORNER_RADIUS') {
|
|
1654
|
+
try {
|
|
1655
|
+
console.log('🌉 [Desktop Bridge] Setting corner radius on node:', msg.nodeId);
|
|
1656
|
+
|
|
1657
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1658
|
+
if (!node) {
|
|
1659
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (!('cornerRadius' in node)) {
|
|
1663
|
+
throw new Error('Node type ' + node.type + ' does not support corner radius');
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
node.cornerRadius = msg.radius;
|
|
1667
|
+
|
|
1668
|
+
console.log('🌉 [Desktop Bridge] Corner radius set to:', msg.radius);
|
|
1669
|
+
|
|
1670
|
+
figma.ui.postMessage({
|
|
1671
|
+
type: 'SET_NODE_CORNER_RADIUS_RESULT',
|
|
1672
|
+
requestId: msg.requestId,
|
|
1673
|
+
success: true,
|
|
1674
|
+
node: { id: node.id, name: node.name, cornerRadius: node.cornerRadius }
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1679
|
+
console.error('🌉 [Desktop Bridge] Set corner radius error:', errorMsg);
|
|
1680
|
+
figma.ui.postMessage({
|
|
1681
|
+
type: 'SET_NODE_CORNER_RADIUS_RESULT',
|
|
1682
|
+
requestId: msg.requestId,
|
|
1683
|
+
success: false,
|
|
1684
|
+
error: errorMsg
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// ============================================================================
|
|
1690
|
+
// CLONE_NODE - Clone/duplicate a node
|
|
1691
|
+
// ============================================================================
|
|
1692
|
+
else if (msg.type === 'CLONE_NODE') {
|
|
1693
|
+
try {
|
|
1694
|
+
console.log('🌉 [Desktop Bridge] Cloning node:', msg.nodeId);
|
|
1695
|
+
|
|
1696
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1697
|
+
if (!node) {
|
|
1698
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
if (!('clone' in node)) {
|
|
1702
|
+
throw new Error('Node type ' + node.type + ' does not support cloning');
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
var clonedNode = node.clone();
|
|
1706
|
+
|
|
1707
|
+
console.log('🌉 [Desktop Bridge] Node cloned:', clonedNode.id);
|
|
1708
|
+
|
|
1709
|
+
figma.ui.postMessage({
|
|
1710
|
+
type: 'CLONE_NODE_RESULT',
|
|
1711
|
+
requestId: msg.requestId,
|
|
1712
|
+
success: true,
|
|
1713
|
+
node: { id: clonedNode.id, name: clonedNode.name, x: clonedNode.x, y: clonedNode.y }
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1718
|
+
console.error('🌉 [Desktop Bridge] Clone node error:', errorMsg);
|
|
1719
|
+
figma.ui.postMessage({
|
|
1720
|
+
type: 'CLONE_NODE_RESULT',
|
|
1721
|
+
requestId: msg.requestId,
|
|
1722
|
+
success: false,
|
|
1723
|
+
error: errorMsg
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// ============================================================================
|
|
1729
|
+
// DELETE_NODE - Delete a node
|
|
1730
|
+
// ============================================================================
|
|
1731
|
+
else if (msg.type === 'DELETE_NODE') {
|
|
1732
|
+
try {
|
|
1733
|
+
console.log('🌉 [Desktop Bridge] Deleting node:', msg.nodeId);
|
|
1734
|
+
|
|
1735
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1736
|
+
if (!node) {
|
|
1737
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
var deletedInfo = { id: node.id, name: node.name };
|
|
1741
|
+
|
|
1742
|
+
node.remove();
|
|
1743
|
+
|
|
1744
|
+
console.log('🌉 [Desktop Bridge] Node deleted');
|
|
1745
|
+
|
|
1746
|
+
figma.ui.postMessage({
|
|
1747
|
+
type: 'DELETE_NODE_RESULT',
|
|
1748
|
+
requestId: msg.requestId,
|
|
1749
|
+
success: true,
|
|
1750
|
+
deleted: deletedInfo
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1755
|
+
console.error('🌉 [Desktop Bridge] Delete node error:', errorMsg);
|
|
1756
|
+
figma.ui.postMessage({
|
|
1757
|
+
type: 'DELETE_NODE_RESULT',
|
|
1758
|
+
requestId: msg.requestId,
|
|
1759
|
+
success: false,
|
|
1760
|
+
error: errorMsg
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// ============================================================================
|
|
1766
|
+
// RENAME_NODE - Rename a node
|
|
1767
|
+
// ============================================================================
|
|
1768
|
+
else if (msg.type === 'RENAME_NODE') {
|
|
1769
|
+
try {
|
|
1770
|
+
console.log('🌉 [Desktop Bridge] Renaming node:', msg.nodeId);
|
|
1771
|
+
|
|
1772
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1773
|
+
if (!node) {
|
|
1774
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
var oldName = node.name;
|
|
1778
|
+
node.name = msg.newName;
|
|
1779
|
+
|
|
1780
|
+
console.log('🌉 [Desktop Bridge] Node renamed from "' + oldName + '" to "' + msg.newName + '"');
|
|
1781
|
+
|
|
1782
|
+
figma.ui.postMessage({
|
|
1783
|
+
type: 'RENAME_NODE_RESULT',
|
|
1784
|
+
requestId: msg.requestId,
|
|
1785
|
+
success: true,
|
|
1786
|
+
node: { id: node.id, name: node.name, oldName: oldName }
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1791
|
+
console.error('🌉 [Desktop Bridge] Rename node error:', errorMsg);
|
|
1792
|
+
figma.ui.postMessage({
|
|
1793
|
+
type: 'RENAME_NODE_RESULT',
|
|
1794
|
+
requestId: msg.requestId,
|
|
1795
|
+
success: false,
|
|
1796
|
+
error: errorMsg
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// ============================================================================
|
|
1802
|
+
// SET_TEXT_CONTENT - Set text on a text node
|
|
1803
|
+
// ============================================================================
|
|
1804
|
+
else if (msg.type === 'SET_TEXT_CONTENT') {
|
|
1805
|
+
try {
|
|
1806
|
+
console.log('🌉 [Desktop Bridge] Setting text content on node:', msg.nodeId);
|
|
1807
|
+
|
|
1808
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1809
|
+
if (!node) {
|
|
1810
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
if (node.type !== 'TEXT') {
|
|
1814
|
+
throw new Error('Node must be a TEXT node. Got: ' + node.type);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// Load the font first
|
|
1818
|
+
await figma.loadFontAsync(node.fontName);
|
|
1819
|
+
|
|
1820
|
+
node.characters = msg.text;
|
|
1821
|
+
|
|
1822
|
+
// Apply font properties if specified
|
|
1823
|
+
if (msg.fontSize) {
|
|
1824
|
+
node.fontSize = msg.fontSize;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
console.log('🌉 [Desktop Bridge] Text content set');
|
|
1828
|
+
|
|
1829
|
+
figma.ui.postMessage({
|
|
1830
|
+
type: 'SET_TEXT_CONTENT_RESULT',
|
|
1831
|
+
requestId: msg.requestId,
|
|
1832
|
+
success: true,
|
|
1833
|
+
node: { id: node.id, name: node.name, characters: node.characters }
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1838
|
+
console.error('🌉 [Desktop Bridge] Set text content error:', errorMsg);
|
|
1839
|
+
figma.ui.postMessage({
|
|
1840
|
+
type: 'SET_TEXT_CONTENT_RESULT',
|
|
1841
|
+
requestId: msg.requestId,
|
|
1842
|
+
success: false,
|
|
1843
|
+
error: errorMsg
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// ============================================================================
|
|
1849
|
+
// CREATE_CHILD_NODE - Create a new child node
|
|
1850
|
+
// ============================================================================
|
|
1851
|
+
else if (msg.type === 'CREATE_CHILD_NODE') {
|
|
1852
|
+
try {
|
|
1853
|
+
console.log('🌉 [Desktop Bridge] Creating child node of type:', msg.nodeType);
|
|
1854
|
+
|
|
1855
|
+
var parent = await figma.getNodeByIdAsync(msg.parentId);
|
|
1856
|
+
if (!parent) {
|
|
1857
|
+
throw new Error('Parent node not found: ' + msg.parentId);
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
if (!('appendChild' in parent)) {
|
|
1861
|
+
throw new Error('Parent node type ' + parent.type + ' does not support children');
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
var newNode;
|
|
1865
|
+
var props = msg.properties || {};
|
|
1866
|
+
|
|
1867
|
+
switch (msg.nodeType) {
|
|
1868
|
+
case 'RECTANGLE':
|
|
1869
|
+
newNode = figma.createRectangle();
|
|
1870
|
+
break;
|
|
1871
|
+
case 'ELLIPSE':
|
|
1872
|
+
newNode = figma.createEllipse();
|
|
1873
|
+
break;
|
|
1874
|
+
case 'FRAME':
|
|
1875
|
+
newNode = figma.createFrame();
|
|
1876
|
+
break;
|
|
1877
|
+
case 'TEXT':
|
|
1878
|
+
newNode = figma.createText();
|
|
1879
|
+
// Load default font
|
|
1880
|
+
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
|
|
1881
|
+
newNode.fontName = { family: 'Inter', style: 'Regular' };
|
|
1882
|
+
if (props.text) {
|
|
1883
|
+
newNode.characters = props.text;
|
|
1884
|
+
}
|
|
1885
|
+
break;
|
|
1886
|
+
case 'LINE':
|
|
1887
|
+
newNode = figma.createLine();
|
|
1888
|
+
break;
|
|
1889
|
+
case 'POLYGON':
|
|
1890
|
+
newNode = figma.createPolygon();
|
|
1891
|
+
break;
|
|
1892
|
+
case 'STAR':
|
|
1893
|
+
newNode = figma.createStar();
|
|
1894
|
+
break;
|
|
1895
|
+
case 'VECTOR':
|
|
1896
|
+
newNode = figma.createVector();
|
|
1897
|
+
break;
|
|
1898
|
+
default:
|
|
1899
|
+
throw new Error('Unsupported node type: ' + msg.nodeType);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// Apply common properties
|
|
1903
|
+
if (props.name) newNode.name = props.name;
|
|
1904
|
+
if (props.x !== undefined) newNode.x = props.x;
|
|
1905
|
+
if (props.y !== undefined) newNode.y = props.y;
|
|
1906
|
+
if (props.width !== undefined && props.height !== undefined) {
|
|
1907
|
+
newNode.resize(props.width, props.height);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// Apply fills if specified
|
|
1911
|
+
if (props.fills) {
|
|
1912
|
+
var processedFills = props.fills.map(function(fill) {
|
|
1913
|
+
if (fill.type === 'SOLID' && typeof fill.color === 'string') {
|
|
1914
|
+
var rgb = hexToFigmaRGB(fill.color);
|
|
1915
|
+
return {
|
|
1916
|
+
type: 'SOLID',
|
|
1917
|
+
color: { r: rgb.r, g: rgb.g, b: rgb.b },
|
|
1918
|
+
opacity: rgb.a !== undefined ? rgb.a : 1
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
return fill;
|
|
1922
|
+
});
|
|
1923
|
+
newNode.fills = processedFills;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// Add to parent
|
|
1927
|
+
parent.appendChild(newNode);
|
|
1928
|
+
|
|
1929
|
+
console.log('🌉 [Desktop Bridge] Child node created:', newNode.id);
|
|
1930
|
+
|
|
1931
|
+
figma.ui.postMessage({
|
|
1932
|
+
type: 'CREATE_CHILD_NODE_RESULT',
|
|
1933
|
+
requestId: msg.requestId,
|
|
1934
|
+
success: true,
|
|
1935
|
+
node: {
|
|
1936
|
+
id: newNode.id,
|
|
1937
|
+
name: newNode.name,
|
|
1938
|
+
type: newNode.type,
|
|
1939
|
+
x: newNode.x,
|
|
1940
|
+
y: newNode.y,
|
|
1941
|
+
width: newNode.width,
|
|
1942
|
+
height: newNode.height
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1948
|
+
console.error('🌉 [Desktop Bridge] Create child node error:', errorMsg);
|
|
1949
|
+
figma.ui.postMessage({
|
|
1950
|
+
type: 'CREATE_CHILD_NODE_RESULT',
|
|
1951
|
+
requestId: msg.requestId,
|
|
1952
|
+
success: false,
|
|
1953
|
+
error: errorMsg
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// ============================================================================
|
|
1959
|
+
// CAPTURE_SCREENSHOT - Capture node screenshot using plugin exportAsync
|
|
1960
|
+
// This captures the CURRENT plugin runtime state (not cloud state like REST API)
|
|
1961
|
+
// ============================================================================
|
|
1962
|
+
else if (msg.type === 'CAPTURE_SCREENSHOT') {
|
|
1963
|
+
try {
|
|
1964
|
+
console.log('🌉 [Desktop Bridge] Capturing screenshot for node:', msg.nodeId);
|
|
1965
|
+
|
|
1966
|
+
var node = msg.nodeId ? await figma.getNodeByIdAsync(msg.nodeId) : figma.currentPage;
|
|
1967
|
+
if (!node) {
|
|
1968
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// Verify node supports export
|
|
1972
|
+
if (!('exportAsync' in node)) {
|
|
1973
|
+
throw new Error('Node type ' + node.type + ' does not support export');
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// Configure export settings
|
|
1977
|
+
var format = msg.format || 'PNG';
|
|
1978
|
+
var scale = msg.scale || 2;
|
|
1979
|
+
|
|
1980
|
+
var exportSettings = {
|
|
1981
|
+
format: format,
|
|
1982
|
+
constraint: { type: 'SCALE', value: scale }
|
|
1983
|
+
};
|
|
1984
|
+
|
|
1985
|
+
// Export the node
|
|
1986
|
+
var bytes = await node.exportAsync(exportSettings);
|
|
1987
|
+
|
|
1988
|
+
// Convert to base64
|
|
1989
|
+
var base64 = figma.base64Encode(bytes);
|
|
1990
|
+
|
|
1991
|
+
// Get node bounds for context
|
|
1992
|
+
var bounds = null;
|
|
1993
|
+
if ('absoluteBoundingBox' in node) {
|
|
1994
|
+
bounds = node.absoluteBoundingBox;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
console.log('🌉 [Desktop Bridge] Screenshot captured:', bytes.length, 'bytes');
|
|
1998
|
+
|
|
1999
|
+
figma.ui.postMessage({
|
|
2000
|
+
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
2001
|
+
requestId: msg.requestId,
|
|
2002
|
+
success: true,
|
|
2003
|
+
image: {
|
|
2004
|
+
base64: base64,
|
|
2005
|
+
format: format,
|
|
2006
|
+
scale: scale,
|
|
2007
|
+
byteLength: bytes.length,
|
|
2008
|
+
node: {
|
|
2009
|
+
id: node.id,
|
|
2010
|
+
name: node.name,
|
|
2011
|
+
type: node.type
|
|
2012
|
+
},
|
|
2013
|
+
bounds: bounds
|
|
2014
|
+
}
|
|
2015
|
+
});
|
|
2016
|
+
|
|
2017
|
+
} catch (error) {
|
|
2018
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
2019
|
+
console.error('🌉 [Desktop Bridge] Screenshot capture error:', errorMsg);
|
|
2020
|
+
figma.ui.postMessage({
|
|
2021
|
+
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
2022
|
+
requestId: msg.requestId,
|
|
2023
|
+
success: false,
|
|
2024
|
+
error: errorMsg
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// ============================================================================
|
|
2030
|
+
// GET_FILE_INFO - Report which file this plugin instance is running in
|
|
2031
|
+
// Used by WebSocket bridge to identify the connected file
|
|
2032
|
+
// ============================================================================
|
|
2033
|
+
else if (msg.type === 'GET_FILE_INFO') {
|
|
2034
|
+
try {
|
|
2035
|
+
var selection = figma.currentPage.selection;
|
|
2036
|
+
figma.ui.postMessage({
|
|
2037
|
+
type: 'GET_FILE_INFO_RESULT',
|
|
2038
|
+
requestId: msg.requestId,
|
|
2039
|
+
success: true,
|
|
2040
|
+
fileInfo: {
|
|
2041
|
+
fileName: figma.root.name,
|
|
2042
|
+
fileKey: figma.fileKey || null,
|
|
2043
|
+
currentPage: figma.currentPage.name,
|
|
2044
|
+
currentPageId: figma.currentPage.id,
|
|
2045
|
+
selectionCount: selection ? selection.length : 0
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
2050
|
+
figma.ui.postMessage({
|
|
2051
|
+
type: 'GET_FILE_INFO_RESULT',
|
|
2052
|
+
requestId: msg.requestId,
|
|
2053
|
+
success: false,
|
|
2054
|
+
error: errorMsg
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// ============================================================================
|
|
2060
|
+
// RESIZE_UI - Dynamically resize the plugin window (e.g., Cloud Mode toggle)
|
|
2061
|
+
// ============================================================================
|
|
2062
|
+
else if (msg.type === 'RESIZE_UI') {
|
|
2063
|
+
figma.ui.resize(msg.width || 120, msg.height || 36);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// ============================================================================
|
|
2067
|
+
// STORE_CLOUD_CONFIG - Persist cloud pairing config in clientStorage
|
|
2068
|
+
// ============================================================================
|
|
2069
|
+
else if (msg.type === 'STORE_CLOUD_CONFIG') {
|
|
2070
|
+
figma.clientStorage.setAsync('cloudConfig', { code: msg.code, timestamp: Date.now() })
|
|
2071
|
+
.catch(function() { /* non-critical */ });
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// ============================================================================
|
|
2075
|
+
// RELOAD_UI - Reload the plugin UI iframe (re-establishes WebSocket connection)
|
|
2076
|
+
// Uses figma.showUI(__html__) to reload without restarting code.js
|
|
2077
|
+
// ============================================================================
|
|
2078
|
+
else if (msg.type === 'RELOAD_UI') {
|
|
2079
|
+
try {
|
|
2080
|
+
console.log('🌉 [Desktop Bridge] Reloading plugin UI');
|
|
2081
|
+
figma.ui.postMessage({
|
|
2082
|
+
type: 'RELOAD_UI_RESULT',
|
|
2083
|
+
requestId: msg.requestId,
|
|
2084
|
+
success: true
|
|
2085
|
+
});
|
|
2086
|
+
// Short delay to let the response message be sent before reload
|
|
2087
|
+
setTimeout(function() {
|
|
2088
|
+
figma.showUI(__html__, { width: 140, height: 50, visible: true, themeColors: true });
|
|
2089
|
+
}, 100);
|
|
2090
|
+
} catch (error) {
|
|
2091
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
2092
|
+
figma.ui.postMessage({
|
|
2093
|
+
type: 'RELOAD_UI_RESULT',
|
|
2094
|
+
requestId: msg.requestId,
|
|
2095
|
+
success: false,
|
|
2096
|
+
error: errorMsg
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
// ============================================================================
|
|
2102
|
+
// SET_INSTANCE_PROPERTIES - Update component properties on an instance
|
|
2103
|
+
// Uses instance.setProperties() to update TEXT, BOOLEAN, INSTANCE_SWAP, VARIANT
|
|
2104
|
+
// ============================================================================
|
|
2105
|
+
else if (msg.type === 'SET_INSTANCE_PROPERTIES') {
|
|
2106
|
+
try {
|
|
2107
|
+
console.log('🌉 [Desktop Bridge] Setting instance properties on:', msg.nodeId);
|
|
2108
|
+
|
|
2109
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
2110
|
+
if (!node) {
|
|
2111
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
if (node.type !== 'INSTANCE') {
|
|
2115
|
+
throw new Error('Node must be an INSTANCE. Got: ' + node.type);
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// Load main component first (required for documentAccess: dynamic-page)
|
|
2119
|
+
var mainComponent = await node.getMainComponentAsync();
|
|
2120
|
+
|
|
2121
|
+
// Get current properties for reference
|
|
2122
|
+
var currentProps = node.componentProperties;
|
|
2123
|
+
console.log('🌉 [Desktop Bridge] Current properties:', JSON.stringify(Object.keys(currentProps)));
|
|
2124
|
+
|
|
2125
|
+
// Build the properties object
|
|
2126
|
+
// Note: TEXT, BOOLEAN, INSTANCE_SWAP properties use the format "PropertyName#nodeId"
|
|
2127
|
+
// VARIANT properties use just "PropertyName"
|
|
2128
|
+
var propsToSet = {};
|
|
2129
|
+
var propUpdates = msg.properties || {};
|
|
2130
|
+
|
|
2131
|
+
for (var propName in propUpdates) {
|
|
2132
|
+
var newValue = propUpdates[propName];
|
|
2133
|
+
|
|
2134
|
+
// Check if this exact property name exists
|
|
2135
|
+
if (currentProps[propName] !== undefined) {
|
|
2136
|
+
propsToSet[propName] = newValue;
|
|
2137
|
+
console.log('🌉 [Desktop Bridge] Setting property:', propName, '=', newValue);
|
|
2138
|
+
} else {
|
|
2139
|
+
// Try to find a matching property with a suffix (for TEXT/BOOLEAN/INSTANCE_SWAP)
|
|
2140
|
+
var foundMatch = false;
|
|
2141
|
+
for (var existingProp in currentProps) {
|
|
2142
|
+
// Check if this is the base property name with a node ID suffix
|
|
2143
|
+
if (existingProp.startsWith(propName + '#')) {
|
|
2144
|
+
propsToSet[existingProp] = newValue;
|
|
2145
|
+
console.log('🌉 [Desktop Bridge] Found suffixed property:', existingProp, '=', newValue);
|
|
2146
|
+
foundMatch = true;
|
|
2147
|
+
break;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
if (!foundMatch) {
|
|
2152
|
+
console.warn('🌉 [Desktop Bridge] Property not found:', propName, '- Available:', Object.keys(currentProps).join(', '));
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
if (Object.keys(propsToSet).length === 0) {
|
|
2158
|
+
throw new Error('No valid properties to set. Available properties: ' + Object.keys(currentProps).join(', '));
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// Apply the properties
|
|
2162
|
+
node.setProperties(propsToSet);
|
|
2163
|
+
|
|
2164
|
+
// Get updated properties
|
|
2165
|
+
var updatedProps = node.componentProperties;
|
|
2166
|
+
|
|
2167
|
+
console.log('🌉 [Desktop Bridge] Instance properties updated');
|
|
2168
|
+
|
|
2169
|
+
figma.ui.postMessage({
|
|
2170
|
+
type: 'SET_INSTANCE_PROPERTIES_RESULT',
|
|
2171
|
+
requestId: msg.requestId,
|
|
2172
|
+
success: true,
|
|
2173
|
+
instance: {
|
|
2174
|
+
id: node.id,
|
|
2175
|
+
name: node.name,
|
|
2176
|
+
componentId: mainComponent ? mainComponent.id : null,
|
|
2177
|
+
propertiesSet: Object.keys(propsToSet),
|
|
2178
|
+
currentProperties: Object.keys(updatedProps).reduce(function(acc, key) {
|
|
2179
|
+
acc[key] = {
|
|
2180
|
+
type: updatedProps[key].type,
|
|
2181
|
+
value: updatedProps[key].value
|
|
2182
|
+
};
|
|
2183
|
+
return acc;
|
|
2184
|
+
}, {})
|
|
2185
|
+
}
|
|
2186
|
+
});
|
|
2187
|
+
|
|
2188
|
+
} catch (error) {
|
|
2189
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
2190
|
+
console.error('🌉 [Desktop Bridge] Set instance properties error:', errorMsg);
|
|
2191
|
+
figma.ui.postMessage({
|
|
2192
|
+
type: 'SET_INSTANCE_PROPERTIES_RESULT',
|
|
2193
|
+
requestId: msg.requestId,
|
|
2194
|
+
success: false,
|
|
2195
|
+
error: errorMsg
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
// ============================================================================
|
|
2201
|
+
// LINT_DESIGN - Accessibility and design quality checks on node tree
|
|
2202
|
+
// ============================================================================
|
|
2203
|
+
else if (msg.type === 'LINT_DESIGN') {
|
|
2204
|
+
try {
|
|
2205
|
+
console.log('🌉 [Desktop Bridge] Running design lint...');
|
|
2206
|
+
|
|
2207
|
+
// ---- Helper functions ----
|
|
2208
|
+
|
|
2209
|
+
// sRGB linearization
|
|
2210
|
+
function lintLinearize(c) {
|
|
2211
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// Relative luminance (r, g, b in 0-1 range)
|
|
2215
|
+
function lintLuminance(r, g, b) {
|
|
2216
|
+
return 0.2126 * lintLinearize(r) + 0.7152 * lintLinearize(g) + 0.0722 * lintLinearize(b);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// Contrast ratio between two colors (each r, g, b in 0-1)
|
|
2220
|
+
function lintContrastRatio(r1, g1, b1, r2, g2, b2) {
|
|
2221
|
+
var l1 = lintLuminance(r1, g1, b1);
|
|
2222
|
+
var l2 = lintLuminance(r2, g2, b2);
|
|
2223
|
+
var lighter = Math.max(l1, l2);
|
|
2224
|
+
var darker = Math.min(l1, l2);
|
|
2225
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
// Convert 0-1 RGB to hex string
|
|
2229
|
+
function lintRgbToHex(r, g, b) {
|
|
2230
|
+
var rr = Math.round(r * 255).toString(16);
|
|
2231
|
+
var gg = Math.round(g * 255).toString(16);
|
|
2232
|
+
var bb = Math.round(b * 255).toString(16);
|
|
2233
|
+
if (rr.length === 1) rr = '0' + rr;
|
|
2234
|
+
if (gg.length === 1) gg = '0' + gg;
|
|
2235
|
+
if (bb.length === 1) bb = '0' + bb;
|
|
2236
|
+
return '#' + rr.toUpperCase() + gg.toUpperCase() + bb.toUpperCase();
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// Walk up ancestors to find nearest solid fill background color
|
|
2240
|
+
function lintGetEffectiveBg(node) {
|
|
2241
|
+
var current = node.parent;
|
|
2242
|
+
while (current) {
|
|
2243
|
+
try {
|
|
2244
|
+
if (current.fills && current.fills.length > 0) {
|
|
2245
|
+
// Iterate reverse (last = topmost visible fill in Figma's stack)
|
|
2246
|
+
for (var fi = current.fills.length - 1; fi >= 0; fi--) {
|
|
2247
|
+
var fill = current.fills[fi];
|
|
2248
|
+
if (fill.type === 'SOLID' && fill.visible !== false) {
|
|
2249
|
+
var opacity = (fill.opacity !== undefined) ? fill.opacity : 1;
|
|
2250
|
+
return { r: fill.color.r, g: fill.color.g, b: fill.color.b, opacity: opacity };
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
} catch (e) {
|
|
2255
|
+
// Slot sublayer — skip
|
|
2256
|
+
}
|
|
2257
|
+
current = current.parent;
|
|
2258
|
+
}
|
|
2259
|
+
// Default to white if no bg found
|
|
2260
|
+
return { r: 1, g: 1, b: 1, opacity: 1 };
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// Check if text qualifies as "large" per WCAG (18pt=24px regular, 14pt≈18.66px bold 700+)
|
|
2264
|
+
function lintIsLargeText(fontSize, fontWeight) {
|
|
2265
|
+
if (fontSize >= 24) return true;
|
|
2266
|
+
if (fontSize >= 18.66 && fontWeight && (fontWeight === 'Bold' || fontWeight === 'Black' || fontWeight === 'ExtraBold')) return true;
|
|
2267
|
+
return false;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// ---- Rule configuration ----
|
|
2271
|
+
var allRuleIds = [
|
|
2272
|
+
'wcag-contrast', 'wcag-text-size', 'wcag-target-size', 'wcag-line-height',
|
|
2273
|
+
'hardcoded-color', 'no-text-style', 'default-name', 'detached-component',
|
|
2274
|
+
'no-autolayout', 'empty-container'
|
|
2275
|
+
];
|
|
2276
|
+
|
|
2277
|
+
var ruleGroups = {
|
|
2278
|
+
'all': allRuleIds,
|
|
2279
|
+
'wcag': ['wcag-contrast', 'wcag-text-size', 'wcag-target-size', 'wcag-line-height'],
|
|
2280
|
+
'design-system': ['hardcoded-color', 'no-text-style', 'default-name', 'detached-component'],
|
|
2281
|
+
'layout': ['no-autolayout', 'empty-container']
|
|
2282
|
+
};
|
|
2283
|
+
|
|
2284
|
+
var severityMap = {
|
|
2285
|
+
'wcag-contrast': 'critical',
|
|
2286
|
+
'wcag-target-size': 'critical',
|
|
2287
|
+
'wcag-text-size': 'warning',
|
|
2288
|
+
'wcag-line-height': 'warning',
|
|
2289
|
+
'hardcoded-color': 'warning',
|
|
2290
|
+
'no-text-style': 'warning',
|
|
2291
|
+
'default-name': 'warning',
|
|
2292
|
+
'detached-component': 'warning',
|
|
2293
|
+
'no-autolayout': 'warning',
|
|
2294
|
+
'empty-container': 'info'
|
|
2295
|
+
};
|
|
2296
|
+
|
|
2297
|
+
var ruleDescriptions = {
|
|
2298
|
+
'wcag-contrast': 'Text does not meet WCAG AA contrast ratio (4.5:1 normal, 3:1 large)',
|
|
2299
|
+
'wcag-text-size': 'Text size is below 12px minimum',
|
|
2300
|
+
'wcag-target-size': 'Interactive element is smaller than 24x24px minimum target size',
|
|
2301
|
+
'wcag-line-height': 'Line height is less than 1.5x the font size',
|
|
2302
|
+
'hardcoded-color': 'Fill color is not bound to a variable or style',
|
|
2303
|
+
'no-text-style': 'Text node is not using a text style',
|
|
2304
|
+
'default-name': 'Node has a default Figma name (e.g., "Frame 1")',
|
|
2305
|
+
'detached-component': 'Frame uses component naming convention but is not a component or instance',
|
|
2306
|
+
'no-autolayout': 'Frame with multiple children does not use auto-layout',
|
|
2307
|
+
'empty-container': 'Frame has no children'
|
|
2308
|
+
};
|
|
2309
|
+
|
|
2310
|
+
var defaultNameRegex = /^(Frame|Rectangle|Ellipse|Line|Text|Group|Component|Instance|Vector|Polygon|Star|Section)(\s+\d+)?$/;
|
|
2311
|
+
var interactiveNameRegex = /button|link|input|checkbox|radio|switch|toggle|tab|menu-item/i;
|
|
2312
|
+
|
|
2313
|
+
// ---- Resolve active rules ----
|
|
2314
|
+
var requestedRules = msg.rules || ['all'];
|
|
2315
|
+
var activeRuleSet = {};
|
|
2316
|
+
for (var ri = 0; ri < requestedRules.length; ri++) {
|
|
2317
|
+
var ruleOrGroup = requestedRules[ri];
|
|
2318
|
+
if (ruleGroups[ruleOrGroup]) {
|
|
2319
|
+
var groupRules = ruleGroups[ruleOrGroup];
|
|
2320
|
+
for (var gi = 0; gi < groupRules.length; gi++) {
|
|
2321
|
+
activeRuleSet[groupRules[gi]] = true;
|
|
2322
|
+
}
|
|
2323
|
+
} else if (severityMap[ruleOrGroup]) {
|
|
2324
|
+
activeRuleSet[ruleOrGroup] = true;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
var maxDepth = typeof msg.maxDepth === 'number' ? msg.maxDepth : 10;
|
|
2329
|
+
var maxFindings = typeof msg.maxFindings === 'number' ? msg.maxFindings : 100;
|
|
2330
|
+
|
|
2331
|
+
// ---- Resolve root node ----
|
|
2332
|
+
var rootNode;
|
|
2333
|
+
if (msg.nodeId) {
|
|
2334
|
+
rootNode = await figma.getNodeByIdAsync(msg.nodeId);
|
|
2335
|
+
if (!rootNode) {
|
|
2336
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
2337
|
+
}
|
|
2338
|
+
} else {
|
|
2339
|
+
rootNode = figma.currentPage;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// ---- Collect context (styles and variables for design-system rules) ----
|
|
2343
|
+
var paintStyleIds = {};
|
|
2344
|
+
var textStyleIds = {};
|
|
2345
|
+
var variableIds = {};
|
|
2346
|
+
|
|
2347
|
+
if (activeRuleSet['hardcoded-color'] || activeRuleSet['no-text-style']) {
|
|
2348
|
+
try {
|
|
2349
|
+
var paintStyles = await figma.getLocalPaintStylesAsync();
|
|
2350
|
+
for (var pi = 0; pi < paintStyles.length; pi++) {
|
|
2351
|
+
paintStyleIds[paintStyles[pi].id] = true;
|
|
2352
|
+
}
|
|
2353
|
+
} catch (e) { /* ignore */ }
|
|
2354
|
+
|
|
2355
|
+
try {
|
|
2356
|
+
var textStyles = await figma.getLocalTextStylesAsync();
|
|
2357
|
+
for (var ti = 0; ti < textStyles.length; ti++) {
|
|
2358
|
+
textStyleIds[textStyles[ti].id] = true;
|
|
2359
|
+
}
|
|
2360
|
+
} catch (e) { /* ignore */ }
|
|
2361
|
+
|
|
2362
|
+
try {
|
|
2363
|
+
var localVars = await figma.variables.getLocalVariablesAsync();
|
|
2364
|
+
for (var vi = 0; vi < localVars.length; vi++) {
|
|
2365
|
+
variableIds[localVars[vi].id] = true;
|
|
2366
|
+
}
|
|
2367
|
+
} catch (e) { /* ignore */ }
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
// ---- Findings storage ----
|
|
2371
|
+
var findings = {};
|
|
2372
|
+
for (var ai = 0; ai < allRuleIds.length; ai++) {
|
|
2373
|
+
if (activeRuleSet[allRuleIds[ai]]) {
|
|
2374
|
+
findings[allRuleIds[ai]] = [];
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
var totalFindings = 0;
|
|
2378
|
+
var nodesScanned = 0;
|
|
2379
|
+
var truncated = false;
|
|
2380
|
+
|
|
2381
|
+
// ---- Tree walk ----
|
|
2382
|
+
function walkNode(node, depth) {
|
|
2383
|
+
if (depth > maxDepth) return;
|
|
2384
|
+
if (truncated) return;
|
|
2385
|
+
|
|
2386
|
+
nodesScanned++;
|
|
2387
|
+
|
|
2388
|
+
var nodeType, nodeName, nodeId;
|
|
2389
|
+
try {
|
|
2390
|
+
nodeType = node.type;
|
|
2391
|
+
nodeName = node.name;
|
|
2392
|
+
nodeId = node.id;
|
|
2393
|
+
} catch (e) {
|
|
2394
|
+
return; // Slot sublayer — skip entirely
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// Skip pages for most checks but still recurse into their children
|
|
2398
|
+
var isPage = nodeType === 'PAGE';
|
|
2399
|
+
var isSection = nodeType === 'SECTION';
|
|
2400
|
+
|
|
2401
|
+
// ---- WCAG checks ----
|
|
2402
|
+
|
|
2403
|
+
// wcag-contrast: TEXT nodes
|
|
2404
|
+
if (activeRuleSet['wcag-contrast'] && nodeType === 'TEXT' && !truncated) {
|
|
2405
|
+
try {
|
|
2406
|
+
var fills = node.fills;
|
|
2407
|
+
if (fills && fills.length > 0) {
|
|
2408
|
+
for (var fci = 0; fci < fills.length; fci++) {
|
|
2409
|
+
if (fills[fci].type === 'SOLID' && fills[fci].visible !== false) {
|
|
2410
|
+
var fg = fills[fci].color;
|
|
2411
|
+
var bg = lintGetEffectiveBg(node);
|
|
2412
|
+
var ratio = lintContrastRatio(fg.r, fg.g, fg.b, bg.r, bg.g, bg.b);
|
|
2413
|
+
var fontSize = 16;
|
|
2414
|
+
var fontWeight = null;
|
|
2415
|
+
try { fontSize = node.fontSize; } catch (e) { /* mixed */ }
|
|
2416
|
+
try { fontWeight = node.fontWeight; } catch (e) { /* mixed */ }
|
|
2417
|
+
if (typeof fontSize !== 'number') fontSize = 16;
|
|
2418
|
+
var isLarge = lintIsLargeText(fontSize, fontWeight);
|
|
2419
|
+
var required = isLarge ? 3.0 : 4.5;
|
|
2420
|
+
var fgOpacity = (fills[fci].opacity !== undefined) ? fills[fci].opacity : 1;
|
|
2421
|
+
var approximate = fgOpacity < 1 || bg.opacity < 1;
|
|
2422
|
+
if (ratio < required) {
|
|
2423
|
+
if (totalFindings < maxFindings) {
|
|
2424
|
+
var finding = {
|
|
2425
|
+
id: nodeId,
|
|
2426
|
+
name: nodeName,
|
|
2427
|
+
ratio: ratio.toFixed(1) + ':1',
|
|
2428
|
+
required: required.toFixed(1) + ':1',
|
|
2429
|
+
fg: lintRgbToHex(fg.r, fg.g, fg.b),
|
|
2430
|
+
bg: lintRgbToHex(bg.r, bg.g, bg.b)
|
|
2431
|
+
};
|
|
2432
|
+
if (approximate) finding.approximate = true;
|
|
2433
|
+
findings['wcag-contrast'].push(finding);
|
|
2434
|
+
totalFindings++;
|
|
2435
|
+
} else {
|
|
2436
|
+
truncated = true;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
break; // Only check the first visible solid fill
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
} catch (e) { /* slot sublayer */ }
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// wcag-text-size: TEXT nodes with fontSize < 12
|
|
2447
|
+
if (activeRuleSet['wcag-text-size'] && nodeType === 'TEXT' && !truncated) {
|
|
2448
|
+
try {
|
|
2449
|
+
var ts = node.fontSize;
|
|
2450
|
+
if (typeof ts === 'number' && ts < 12) {
|
|
2451
|
+
if (totalFindings < maxFindings) {
|
|
2452
|
+
findings['wcag-text-size'].push({
|
|
2453
|
+
id: nodeId,
|
|
2454
|
+
name: nodeName,
|
|
2455
|
+
fontSize: ts
|
|
2456
|
+
});
|
|
2457
|
+
totalFindings++;
|
|
2458
|
+
} else {
|
|
2459
|
+
truncated = true;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
} catch (e) { /* slot sublayer or mixed */ }
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// wcag-target-size: Interactive elements < 24x24
|
|
2466
|
+
if (activeRuleSet['wcag-target-size'] && !isPage && !isSection && !truncated) {
|
|
2467
|
+
try {
|
|
2468
|
+
if ((nodeType === 'FRAME' || nodeType === 'COMPONENT' || nodeType === 'INSTANCE' || nodeType === 'COMPONENT_SET') && interactiveNameRegex.test(nodeName)) {
|
|
2469
|
+
var tw = node.width;
|
|
2470
|
+
var th = node.height;
|
|
2471
|
+
if ((typeof tw === 'number' && tw < 24) || (typeof th === 'number' && th < 24)) {
|
|
2472
|
+
if (totalFindings < maxFindings) {
|
|
2473
|
+
findings['wcag-target-size'].push({
|
|
2474
|
+
id: nodeId,
|
|
2475
|
+
name: nodeName,
|
|
2476
|
+
width: tw,
|
|
2477
|
+
height: th
|
|
2478
|
+
});
|
|
2479
|
+
totalFindings++;
|
|
2480
|
+
} else {
|
|
2481
|
+
truncated = true;
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
} catch (e) { /* slot sublayer */ }
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// wcag-line-height: TEXT nodes where lineHeight < 1.5 * fontSize
|
|
2489
|
+
if (activeRuleSet['wcag-line-height'] && nodeType === 'TEXT' && !truncated) {
|
|
2490
|
+
try {
|
|
2491
|
+
var lh = node.lineHeight;
|
|
2492
|
+
var fs = node.fontSize;
|
|
2493
|
+
var effectiveLh = null;
|
|
2494
|
+
if (lh && typeof fs === 'number' && typeof lh === 'object' && typeof lh.value === 'number') {
|
|
2495
|
+
if (lh.unit === 'PIXELS') {
|
|
2496
|
+
effectiveLh = lh.value;
|
|
2497
|
+
} else if (lh.unit === 'PERCENT') {
|
|
2498
|
+
effectiveLh = fs * (lh.value / 100);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
if (effectiveLh !== null && effectiveLh < 1.5 * fs) {
|
|
2502
|
+
if (totalFindings < maxFindings) {
|
|
2503
|
+
findings['wcag-line-height'].push({
|
|
2504
|
+
id: nodeId,
|
|
2505
|
+
name: nodeName,
|
|
2506
|
+
lineHeight: effectiveLh,
|
|
2507
|
+
fontSize: fs,
|
|
2508
|
+
recommended: (1.5 * fs).toFixed(1)
|
|
2509
|
+
});
|
|
2510
|
+
totalFindings++;
|
|
2511
|
+
} else {
|
|
2512
|
+
truncated = true;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
} catch (e) { /* slot sublayer or mixed */ }
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// ---- Design System checks ----
|
|
2519
|
+
|
|
2520
|
+
// hardcoded-color: Solid fills without variable binding or style
|
|
2521
|
+
if (activeRuleSet['hardcoded-color'] && !isPage && !isSection && !truncated) {
|
|
2522
|
+
try {
|
|
2523
|
+
var hcFills = node.fills;
|
|
2524
|
+
if (hcFills && hcFills.length > 0) {
|
|
2525
|
+
var hasFillStyle = false;
|
|
2526
|
+
try {
|
|
2527
|
+
hasFillStyle = node.fillStyleId && node.fillStyleId !== '';
|
|
2528
|
+
} catch (e) { /* mixed fill styles */ }
|
|
2529
|
+
|
|
2530
|
+
if (!hasFillStyle) {
|
|
2531
|
+
for (var hci = 0; hci < hcFills.length; hci++) {
|
|
2532
|
+
var hcFill = hcFills[hci];
|
|
2533
|
+
if (hcFill.type === 'SOLID' && hcFill.visible !== false) {
|
|
2534
|
+
var hasBoundVar = false;
|
|
2535
|
+
try {
|
|
2536
|
+
if (hcFill.boundVariables && hcFill.boundVariables.color) {
|
|
2537
|
+
hasBoundVar = true;
|
|
2538
|
+
}
|
|
2539
|
+
} catch (e) { /* no bound vars */ }
|
|
2540
|
+
|
|
2541
|
+
if (!hasBoundVar) {
|
|
2542
|
+
if (totalFindings < maxFindings) {
|
|
2543
|
+
findings['hardcoded-color'].push({
|
|
2544
|
+
id: nodeId,
|
|
2545
|
+
name: nodeName,
|
|
2546
|
+
color: lintRgbToHex(hcFill.color.r, hcFill.color.g, hcFill.color.b)
|
|
2547
|
+
});
|
|
2548
|
+
totalFindings++;
|
|
2549
|
+
} else {
|
|
2550
|
+
truncated = true;
|
|
2551
|
+
}
|
|
2552
|
+
break; // One finding per node
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
} catch (e) { /* slot sublayer */ }
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
// no-text-style: TEXT nodes without textStyleId
|
|
2562
|
+
if (activeRuleSet['no-text-style'] && nodeType === 'TEXT' && !truncated) {
|
|
2563
|
+
try {
|
|
2564
|
+
var hasTextStyle = node.textStyleId && node.textStyleId !== '';
|
|
2565
|
+
if (!hasTextStyle) {
|
|
2566
|
+
if (totalFindings < maxFindings) {
|
|
2567
|
+
findings['no-text-style'].push({
|
|
2568
|
+
id: nodeId,
|
|
2569
|
+
name: nodeName
|
|
2570
|
+
});
|
|
2571
|
+
totalFindings++;
|
|
2572
|
+
} else {
|
|
2573
|
+
truncated = true;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
} catch (e) { /* slot sublayer or mixed */ }
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
// default-name: Nodes with default Figma names
|
|
2580
|
+
if (activeRuleSet['default-name'] && !isPage && !truncated) {
|
|
2581
|
+
try {
|
|
2582
|
+
if (defaultNameRegex.test(nodeName)) {
|
|
2583
|
+
if (totalFindings < maxFindings) {
|
|
2584
|
+
findings['default-name'].push({
|
|
2585
|
+
id: nodeId,
|
|
2586
|
+
name: nodeName,
|
|
2587
|
+
type: nodeType
|
|
2588
|
+
});
|
|
2589
|
+
totalFindings++;
|
|
2590
|
+
} else {
|
|
2591
|
+
truncated = true;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
} catch (e) { /* slot sublayer */ }
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// detached-component: Frames with "/" in name but not component/instance
|
|
2598
|
+
if (activeRuleSet['detached-component'] && nodeType === 'FRAME' && !truncated) {
|
|
2599
|
+
try {
|
|
2600
|
+
if (nodeName.indexOf('/') !== -1) {
|
|
2601
|
+
if (totalFindings < maxFindings) {
|
|
2602
|
+
findings['detached-component'].push({
|
|
2603
|
+
id: nodeId,
|
|
2604
|
+
name: nodeName
|
|
2605
|
+
});
|
|
2606
|
+
totalFindings++;
|
|
2607
|
+
} else {
|
|
2608
|
+
truncated = true;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
} catch (e) { /* slot sublayer */ }
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// ---- Layout checks ----
|
|
2615
|
+
|
|
2616
|
+
// no-autolayout: Frames with 2+ children and no auto-layout
|
|
2617
|
+
if (activeRuleSet['no-autolayout'] && !isPage && !isSection && !truncated) {
|
|
2618
|
+
try {
|
|
2619
|
+
if (nodeType === 'FRAME' || nodeType === 'COMPONENT' || nodeType === 'COMPONENT_SET') {
|
|
2620
|
+
var childCount = 0;
|
|
2621
|
+
try { childCount = node.children ? node.children.length : 0; } catch (e) { /* skip */ }
|
|
2622
|
+
if (childCount >= 2) {
|
|
2623
|
+
var layoutMode = 'NONE';
|
|
2624
|
+
try { layoutMode = node.layoutMode; } catch (e) { /* skip */ }
|
|
2625
|
+
if (!layoutMode || layoutMode === 'NONE') {
|
|
2626
|
+
if (totalFindings < maxFindings) {
|
|
2627
|
+
findings['no-autolayout'].push({
|
|
2628
|
+
id: nodeId,
|
|
2629
|
+
name: nodeName,
|
|
2630
|
+
childCount: childCount
|
|
2631
|
+
});
|
|
2632
|
+
totalFindings++;
|
|
2633
|
+
} else {
|
|
2634
|
+
truncated = true;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
} catch (e) { /* slot sublayer */ }
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
// empty-container: Frames with zero children
|
|
2643
|
+
if (activeRuleSet['empty-container'] && !isPage && !isSection && !truncated) {
|
|
2644
|
+
try {
|
|
2645
|
+
if (nodeType === 'FRAME') {
|
|
2646
|
+
var ec = 0;
|
|
2647
|
+
try { ec = node.children ? node.children.length : 0; } catch (e) { /* skip */ }
|
|
2648
|
+
if (ec === 0) {
|
|
2649
|
+
if (totalFindings < maxFindings) {
|
|
2650
|
+
findings['empty-container'].push({
|
|
2651
|
+
id: nodeId,
|
|
2652
|
+
name: nodeName
|
|
2653
|
+
});
|
|
2654
|
+
totalFindings++;
|
|
2655
|
+
} else {
|
|
2656
|
+
truncated = true;
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
} catch (e) { /* slot sublayer */ }
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
// ---- Recurse into children ----
|
|
2664
|
+
try {
|
|
2665
|
+
if (node.children) {
|
|
2666
|
+
for (var ci = 0; ci < node.children.length; ci++) {
|
|
2667
|
+
if (truncated) break;
|
|
2668
|
+
walkNode(node.children[ci], depth + 1);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
} catch (e) { /* no children or slot sublayer */ }
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// ---- Execute walk ----
|
|
2675
|
+
walkNode(rootNode, 0);
|
|
2676
|
+
|
|
2677
|
+
// ---- Build response ----
|
|
2678
|
+
var categories = [];
|
|
2679
|
+
var summaryObj = { critical: 0, warning: 0, info: 0, total: 0 };
|
|
2680
|
+
|
|
2681
|
+
for (var rk = 0; rk < allRuleIds.length; rk++) {
|
|
2682
|
+
var ruleId = allRuleIds[rk];
|
|
2683
|
+
if (!findings[ruleId] || findings[ruleId].length === 0) continue;
|
|
2684
|
+
var sev = severityMap[ruleId];
|
|
2685
|
+
categories.push({
|
|
2686
|
+
rule: ruleId,
|
|
2687
|
+
severity: sev,
|
|
2688
|
+
count: findings[ruleId].length,
|
|
2689
|
+
description: ruleDescriptions[ruleId],
|
|
2690
|
+
nodes: findings[ruleId]
|
|
2691
|
+
});
|
|
2692
|
+
summaryObj[sev] = (summaryObj[sev] || 0) + findings[ruleId].length;
|
|
2693
|
+
summaryObj.total += findings[ruleId].length;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
var responseData = {
|
|
2697
|
+
rootNodeId: rootNode.id,
|
|
2698
|
+
rootNodeName: rootNode.name,
|
|
2699
|
+
nodesScanned: nodesScanned,
|
|
2700
|
+
categories: categories,
|
|
2701
|
+
summary: summaryObj
|
|
2702
|
+
};
|
|
2703
|
+
|
|
2704
|
+
if (truncated) {
|
|
2705
|
+
responseData.warning = 'Showing first ' + maxFindings + ' findings...';
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
console.log('🌉 [Desktop Bridge] Lint complete: ' + summaryObj.total + ' findings across ' + nodesScanned + ' nodes');
|
|
2709
|
+
|
|
2710
|
+
figma.ui.postMessage({
|
|
2711
|
+
type: 'LINT_DESIGN_RESULT',
|
|
2712
|
+
requestId: msg.requestId,
|
|
2713
|
+
success: true,
|
|
2714
|
+
data: responseData
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
} catch (error) {
|
|
2718
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
2719
|
+
console.error('🌉 [Desktop Bridge] Lint design error:', errorMsg);
|
|
2720
|
+
figma.ui.postMessage({
|
|
2721
|
+
type: 'LINT_DESIGN_RESULT',
|
|
2722
|
+
requestId: msg.requestId,
|
|
2723
|
+
success: false,
|
|
2724
|
+
error: errorMsg
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
};
|
|
2729
|
+
|
|
2730
|
+
// ============================================================================
|
|
2731
|
+
// DOCUMENT CHANGE LISTENER - Forward change events for cache invalidation
|
|
2732
|
+
// Fires when variables, styles, or nodes change (by any means — user edits, API, etc.)
|
|
2733
|
+
// Requires figma.loadAllPagesAsync() in dynamic-page mode before registering.
|
|
2734
|
+
// ============================================================================
|
|
2735
|
+
figma.loadAllPagesAsync().then(function() {
|
|
2736
|
+
figma.on('documentchange', function(event) {
|
|
2737
|
+
var hasStyleChanges = false;
|
|
2738
|
+
var hasNodeChanges = false;
|
|
2739
|
+
var changedNodeIds = [];
|
|
2740
|
+
|
|
2741
|
+
for (var i = 0; i < event.documentChanges.length; i++) {
|
|
2742
|
+
var change = event.documentChanges[i];
|
|
2743
|
+
if (change.type === 'STYLE_CREATE' || change.type === 'STYLE_DELETE' || change.type === 'STYLE_PROPERTY_CHANGE') {
|
|
2744
|
+
hasStyleChanges = true;
|
|
2745
|
+
} else if (change.type === 'CREATE' || change.type === 'DELETE' || change.type === 'PROPERTY_CHANGE') {
|
|
2746
|
+
hasNodeChanges = true;
|
|
2747
|
+
if (change.id && changedNodeIds.length < 50) {
|
|
2748
|
+
changedNodeIds.push(change.id);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
if (hasStyleChanges || hasNodeChanges) {
|
|
2754
|
+
figma.ui.postMessage({
|
|
2755
|
+
type: 'DOCUMENT_CHANGE',
|
|
2756
|
+
data: {
|
|
2757
|
+
hasStyleChanges: hasStyleChanges,
|
|
2758
|
+
hasNodeChanges: hasNodeChanges,
|
|
2759
|
+
changedNodeIds: changedNodeIds,
|
|
2760
|
+
changeCount: event.documentChanges.length,
|
|
2761
|
+
timestamp: Date.now()
|
|
2762
|
+
}
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
});
|
|
2766
|
+
// Selection change listener — tracks what the user has selected in Figma
|
|
2767
|
+
figma.on('selectionchange', function() {
|
|
2768
|
+
var selection = figma.currentPage.selection;
|
|
2769
|
+
var selectedNodes = [];
|
|
2770
|
+
for (var i = 0; i < Math.min(selection.length, 50); i++) {
|
|
2771
|
+
try {
|
|
2772
|
+
var node = selection[i];
|
|
2773
|
+
selectedNodes.push({
|
|
2774
|
+
id: node.id,
|
|
2775
|
+
name: node.name,
|
|
2776
|
+
type: node.type,
|
|
2777
|
+
width: node.width,
|
|
2778
|
+
height: node.height
|
|
2779
|
+
});
|
|
2780
|
+
} catch (e) {
|
|
2781
|
+
// Slot sublayers and table cells may not be fully resolvable —
|
|
2782
|
+
// accessing .name throws "does not exist" for these node types.
|
|
2783
|
+
// Skip silently rather than crashing the plugin.
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
figma.ui.postMessage({
|
|
2787
|
+
type: 'SELECTION_CHANGE',
|
|
2788
|
+
data: {
|
|
2789
|
+
nodes: selectedNodes,
|
|
2790
|
+
count: selection.length,
|
|
2791
|
+
page: figma.currentPage.name,
|
|
2792
|
+
timestamp: Date.now()
|
|
2793
|
+
}
|
|
2794
|
+
});
|
|
2795
|
+
});
|
|
2796
|
+
|
|
2797
|
+
// Page change listener — tracks which page the user is viewing
|
|
2798
|
+
figma.on('currentpagechange', function() {
|
|
2799
|
+
figma.ui.postMessage({
|
|
2800
|
+
type: 'PAGE_CHANGE',
|
|
2801
|
+
data: {
|
|
2802
|
+
pageId: figma.currentPage.id,
|
|
2803
|
+
pageName: figma.currentPage.name,
|
|
2804
|
+
timestamp: Date.now()
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
});
|
|
2808
|
+
|
|
2809
|
+
console.log('🌉 [Desktop Bridge] Document change, selection, and page listeners registered');
|
|
2810
|
+
}).catch(function(err) {
|
|
2811
|
+
console.warn('🌉 [Desktop Bridge] Could not register event listeners:', err);
|
|
2812
|
+
});
|
|
2813
|
+
|
|
2814
|
+
console.log('🌉 [Desktop Bridge] Ready to handle component requests');
|
|
2815
|
+
console.log('🌉 [Desktop Bridge] Plugin will stay open until manually closed');
|
|
2816
|
+
|
|
2817
|
+
// Plugin stays open - no auto-close
|
|
2818
|
+
// UI iframe remains accessible for Puppeteer to read data from window object
|