@mp3wizard/figma-console-mcp 1.14.0

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