@muloka/figma-console-mcp 0.1.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 (443) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +898 -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-manager.js +157 -0
  62. package/dist/cloudflare/core/accessibility-tools.js +361 -0
  63. package/dist/cloudflare/core/annotation-tools.js +230 -0
  64. package/dist/cloudflare/core/cloud-websocket-connector.js +419 -0
  65. package/dist/cloudflare/core/cloud-websocket-relay.js +198 -0
  66. package/dist/cloudflare/core/comment-tools.js +292 -0
  67. package/dist/cloudflare/core/config.js +153 -0
  68. package/dist/cloudflare/core/console-monitor.js +427 -0
  69. package/dist/cloudflare/core/deep-component-tools.js +128 -0
  70. package/dist/cloudflare/core/design-code-tools.js +2793 -0
  71. package/dist/cloudflare/core/design-system-manifest.js +265 -0
  72. package/dist/cloudflare/core/design-system-tools.js +882 -0
  73. package/dist/cloudflare/core/diagnose-tool.js +100 -0
  74. package/dist/cloudflare/core/diff/changelog-formatter.js +275 -0
  75. package/dist/cloudflare/core/diff/diff-engine.js +334 -0
  76. package/dist/cloudflare/core/diff/property-compare.js +36 -0
  77. package/dist/cloudflare/core/diff/version-cache.js +74 -0
  78. package/dist/cloudflare/core/enrichment/enrichment-service.js +374 -0
  79. package/dist/cloudflare/core/enrichment/index.js +7 -0
  80. package/dist/cloudflare/core/enrichment/relationship-mapper.js +351 -0
  81. package/dist/cloudflare/core/enrichment/style-resolver.js +346 -0
  82. package/dist/cloudflare/core/figjam-tools.js +547 -0
  83. package/dist/cloudflare/core/figma-api.js +509 -0
  84. package/dist/cloudflare/core/figma-connector.js +7 -0
  85. package/dist/cloudflare/core/figma-reconstruction-spec.js +402 -0
  86. package/dist/cloudflare/core/figma-style-extractor.js +311 -0
  87. package/dist/cloudflare/core/figma-tools.js +3302 -0
  88. package/dist/cloudflare/core/identity.js +96 -0
  89. package/dist/cloudflare/core/library-tools.js +580 -0
  90. package/dist/cloudflare/core/logger.js +53 -0
  91. package/dist/cloudflare/core/port-discovery.js +935 -0
  92. package/dist/cloudflare/core/resolve-package-root.js +11 -0
  93. package/dist/cloudflare/core/slides-tools.js +714 -0
  94. package/dist/cloudflare/core/slot-tools.js +334 -0
  95. package/dist/cloudflare/core/snippet-injector.js +96 -0
  96. package/dist/cloudflare/core/tokens/alias-resolver.js +205 -0
  97. package/dist/cloudflare/core/tokens/config.js +294 -0
  98. package/dist/cloudflare/core/tokens/dialect.js +232 -0
  99. package/dist/cloudflare/core/tokens/figma-converter.js +323 -0
  100. package/dist/cloudflare/core/tokens/formatters/css-vars.js +338 -0
  101. package/dist/cloudflare/core/tokens/formatters/dtcg.js +376 -0
  102. package/dist/cloudflare/core/tokens/formatters/index.js +45 -0
  103. package/dist/cloudflare/core/tokens/formatters/json.js +205 -0
  104. package/dist/cloudflare/core/tokens/formatters/less.js +4 -0
  105. package/dist/cloudflare/core/tokens/formatters/scss.js +258 -0
  106. package/dist/cloudflare/core/tokens/formatters/stubs.js +13 -0
  107. package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +213 -0
  108. package/dist/cloudflare/core/tokens/formatters/tailwind-v3.js +237 -0
  109. package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +335 -0
  110. package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +256 -0
  111. package/dist/cloudflare/core/tokens/formatters/ts-module.js +198 -0
  112. package/dist/cloudflare/core/tokens/index.js +16 -0
  113. package/dist/cloudflare/core/tokens/parsers/css-vars.js +4 -0
  114. package/dist/cloudflare/core/tokens/parsers/dtcg.js +280 -0
  115. package/dist/cloudflare/core/tokens/parsers/index.js +138 -0
  116. package/dist/cloudflare/core/tokens/parsers/json.js +7 -0
  117. package/dist/cloudflare/core/tokens/parsers/scss.js +4 -0
  118. package/dist/cloudflare/core/tokens/parsers/stubs.js +20 -0
  119. package/dist/cloudflare/core/tokens/parsers/style-dictionary-v3.js +4 -0
  120. package/dist/cloudflare/core/tokens/parsers/tailwind-v3.js +4 -0
  121. package/dist/cloudflare/core/tokens/parsers/tailwind-v4.js +4 -0
  122. package/dist/cloudflare/core/tokens/parsers/tokens-studio.js +4 -0
  123. package/dist/cloudflare/core/tokens/schemas.js +152 -0
  124. package/dist/cloudflare/core/tokens/transforms/color.js +12 -0
  125. package/dist/cloudflare/core/tokens/transforms/index.js +29 -0
  126. package/dist/cloudflare/core/tokens/transforms/size.js +7 -0
  127. package/dist/cloudflare/core/tokens/types.js +18 -0
  128. package/dist/cloudflare/core/tokens-tools.js +1788 -0
  129. package/dist/cloudflare/core/types/design-code.js +4 -0
  130. package/dist/cloudflare/core/types/enriched.js +5 -0
  131. package/dist/cloudflare/core/types/index.js +4 -0
  132. package/dist/cloudflare/core/variable-resolver.js +85 -0
  133. package/dist/cloudflare/core/version-tools.js +1199 -0
  134. package/dist/cloudflare/core/websocket-connector.js +436 -0
  135. package/dist/cloudflare/core/websocket-server.js +956 -0
  136. package/dist/cloudflare/core/write-tools.js +2506 -0
  137. package/dist/cloudflare/index.js +3010 -0
  138. package/dist/core/accessibility-tools.d.ts +21 -0
  139. package/dist/core/accessibility-tools.d.ts.map +1 -0
  140. package/dist/core/accessibility-tools.js +362 -0
  141. package/dist/core/accessibility-tools.js.map +1 -0
  142. package/dist/core/annotation-tools.d.ts +14 -0
  143. package/dist/core/annotation-tools.d.ts.map +1 -0
  144. package/dist/core/annotation-tools.js +231 -0
  145. package/dist/core/annotation-tools.js.map +1 -0
  146. package/dist/core/comment-tools.d.ts +11 -0
  147. package/dist/core/comment-tools.d.ts.map +1 -0
  148. package/dist/core/comment-tools.js +293 -0
  149. package/dist/core/comment-tools.js.map +1 -0
  150. package/dist/core/config.d.ts +17 -0
  151. package/dist/core/config.d.ts.map +1 -0
  152. package/dist/core/config.js +154 -0
  153. package/dist/core/config.js.map +1 -0
  154. package/dist/core/console-monitor.d.ts +82 -0
  155. package/dist/core/console-monitor.d.ts.map +1 -0
  156. package/dist/core/console-monitor.js +428 -0
  157. package/dist/core/console-monitor.js.map +1 -0
  158. package/dist/core/deep-component-tools.d.ts +14 -0
  159. package/dist/core/deep-component-tools.d.ts.map +1 -0
  160. package/dist/core/deep-component-tools.js +129 -0
  161. package/dist/core/deep-component-tools.js.map +1 -0
  162. package/dist/core/design-code-tools.d.ts +116 -0
  163. package/dist/core/design-code-tools.d.ts.map +1 -0
  164. package/dist/core/design-code-tools.js +2794 -0
  165. package/dist/core/design-code-tools.js.map +1 -0
  166. package/dist/core/design-system-manifest.d.ts +273 -0
  167. package/dist/core/design-system-manifest.d.ts.map +1 -0
  168. package/dist/core/design-system-manifest.js +266 -0
  169. package/dist/core/design-system-manifest.js.map +1 -0
  170. package/dist/core/design-system-tools.d.ts +67 -0
  171. package/dist/core/design-system-tools.d.ts.map +1 -0
  172. package/dist/core/design-system-tools.js +883 -0
  173. package/dist/core/design-system-tools.js.map +1 -0
  174. package/dist/core/diagnose-tool.d.ts +41 -0
  175. package/dist/core/diagnose-tool.d.ts.map +1 -0
  176. package/dist/core/diagnose-tool.js +101 -0
  177. package/dist/core/diagnose-tool.js.map +1 -0
  178. package/dist/core/diff/changelog-formatter.d.ts +35 -0
  179. package/dist/core/diff/changelog-formatter.d.ts.map +1 -0
  180. package/dist/core/diff/changelog-formatter.js +276 -0
  181. package/dist/core/diff/changelog-formatter.js.map +1 -0
  182. package/dist/core/diff/diff-engine.d.ts +127 -0
  183. package/dist/core/diff/diff-engine.d.ts.map +1 -0
  184. package/dist/core/diff/diff-engine.js +335 -0
  185. package/dist/core/diff/diff-engine.js.map +1 -0
  186. package/dist/core/diff/property-compare.d.ts +19 -0
  187. package/dist/core/diff/property-compare.d.ts.map +1 -0
  188. package/dist/core/diff/property-compare.js +37 -0
  189. package/dist/core/diff/property-compare.js.map +1 -0
  190. package/dist/core/diff/version-cache.d.ts +40 -0
  191. package/dist/core/diff/version-cache.d.ts.map +1 -0
  192. package/dist/core/diff/version-cache.js +75 -0
  193. package/dist/core/diff/version-cache.js.map +1 -0
  194. package/dist/core/enrichment/enrichment-service.d.ts +52 -0
  195. package/dist/core/enrichment/enrichment-service.d.ts.map +1 -0
  196. package/dist/core/enrichment/enrichment-service.js +375 -0
  197. package/dist/core/enrichment/enrichment-service.js.map +1 -0
  198. package/dist/core/enrichment/index.d.ts +8 -0
  199. package/dist/core/enrichment/index.d.ts.map +1 -0
  200. package/dist/core/enrichment/index.js +8 -0
  201. package/dist/core/enrichment/index.js.map +1 -0
  202. package/dist/core/enrichment/relationship-mapper.d.ts +106 -0
  203. package/dist/core/enrichment/relationship-mapper.d.ts.map +1 -0
  204. package/dist/core/enrichment/relationship-mapper.js +352 -0
  205. package/dist/core/enrichment/relationship-mapper.js.map +1 -0
  206. package/dist/core/enrichment/style-resolver.d.ts +85 -0
  207. package/dist/core/enrichment/style-resolver.d.ts.map +1 -0
  208. package/dist/core/enrichment/style-resolver.js +347 -0
  209. package/dist/core/enrichment/style-resolver.js.map +1 -0
  210. package/dist/core/figjam-tools.d.ts +8 -0
  211. package/dist/core/figjam-tools.d.ts.map +1 -0
  212. package/dist/core/figjam-tools.js +548 -0
  213. package/dist/core/figjam-tools.js.map +1 -0
  214. package/dist/core/figma-api.d.ts +254 -0
  215. package/dist/core/figma-api.d.ts.map +1 -0
  216. package/dist/core/figma-api.js +510 -0
  217. package/dist/core/figma-api.js.map +1 -0
  218. package/dist/core/figma-connector.d.ts +214 -0
  219. package/dist/core/figma-connector.d.ts.map +1 -0
  220. package/dist/core/figma-connector.js +8 -0
  221. package/dist/core/figma-connector.js.map +1 -0
  222. package/dist/core/figma-desktop-connector.d.ts +312 -0
  223. package/dist/core/figma-desktop-connector.d.ts.map +1 -0
  224. package/dist/core/figma-desktop-connector.js +1298 -0
  225. package/dist/core/figma-desktop-connector.js.map +1 -0
  226. package/dist/core/figma-reconstruction-spec.d.ts +166 -0
  227. package/dist/core/figma-reconstruction-spec.d.ts.map +1 -0
  228. package/dist/core/figma-reconstruction-spec.js +403 -0
  229. package/dist/core/figma-reconstruction-spec.js.map +1 -0
  230. package/dist/core/figma-style-extractor.d.ts +76 -0
  231. package/dist/core/figma-style-extractor.d.ts.map +1 -0
  232. package/dist/core/figma-style-extractor.js +312 -0
  233. package/dist/core/figma-style-extractor.js.map +1 -0
  234. package/dist/core/figma-tools.d.ts +22 -0
  235. package/dist/core/figma-tools.d.ts.map +1 -0
  236. package/dist/core/figma-tools.js +3303 -0
  237. package/dist/core/figma-tools.js.map +1 -0
  238. package/dist/core/identity.d.ts +41 -0
  239. package/dist/core/identity.d.ts.map +1 -0
  240. package/dist/core/identity.js +97 -0
  241. package/dist/core/identity.js.map +1 -0
  242. package/dist/core/library-tools.d.ts +17 -0
  243. package/dist/core/library-tools.d.ts.map +1 -0
  244. package/dist/core/library-tools.js +581 -0
  245. package/dist/core/library-tools.js.map +1 -0
  246. package/dist/core/logger.d.ts +22 -0
  247. package/dist/core/logger.d.ts.map +1 -0
  248. package/dist/core/logger.js +54 -0
  249. package/dist/core/logger.js.map +1 -0
  250. package/dist/core/port-discovery.d.ts +211 -0
  251. package/dist/core/port-discovery.d.ts.map +1 -0
  252. package/dist/core/port-discovery.js +936 -0
  253. package/dist/core/port-discovery.js.map +1 -0
  254. package/dist/core/resolve-package-root.d.ts +2 -0
  255. package/dist/core/resolve-package-root.d.ts.map +1 -0
  256. package/dist/core/resolve-package-root.js +12 -0
  257. package/dist/core/resolve-package-root.js.map +1 -0
  258. package/dist/core/slides-tools.d.ts +8 -0
  259. package/dist/core/slides-tools.d.ts.map +1 -0
  260. package/dist/core/slides-tools.js +715 -0
  261. package/dist/core/slides-tools.js.map +1 -0
  262. package/dist/core/slot-tools.d.ts +7 -0
  263. package/dist/core/slot-tools.d.ts.map +1 -0
  264. package/dist/core/slot-tools.js +335 -0
  265. package/dist/core/slot-tools.js.map +1 -0
  266. package/dist/core/snippet-injector.d.ts +24 -0
  267. package/dist/core/snippet-injector.d.ts.map +1 -0
  268. package/dist/core/snippet-injector.js +97 -0
  269. package/dist/core/snippet-injector.js.map +1 -0
  270. package/dist/core/tokens/alias-resolver.d.ts +97 -0
  271. package/dist/core/tokens/alias-resolver.d.ts.map +1 -0
  272. package/dist/core/tokens/alias-resolver.js +206 -0
  273. package/dist/core/tokens/alias-resolver.js.map +1 -0
  274. package/dist/core/tokens/config.d.ts +380 -0
  275. package/dist/core/tokens/config.d.ts.map +1 -0
  276. package/dist/core/tokens/config.js +295 -0
  277. package/dist/core/tokens/config.js.map +1 -0
  278. package/dist/core/tokens/dialect.d.ts +107 -0
  279. package/dist/core/tokens/dialect.d.ts.map +1 -0
  280. package/dist/core/tokens/dialect.js +233 -0
  281. package/dist/core/tokens/dialect.js.map +1 -0
  282. package/dist/core/tokens/figma-converter.d.ts +102 -0
  283. package/dist/core/tokens/figma-converter.d.ts.map +1 -0
  284. package/dist/core/tokens/figma-converter.js +324 -0
  285. package/dist/core/tokens/figma-converter.js.map +1 -0
  286. package/dist/core/tokens/formatters/css-vars.d.ts +24 -0
  287. package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -0
  288. package/dist/core/tokens/formatters/css-vars.js +339 -0
  289. package/dist/core/tokens/formatters/css-vars.js.map +1 -0
  290. package/dist/core/tokens/formatters/dtcg.d.ts +28 -0
  291. package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -0
  292. package/dist/core/tokens/formatters/dtcg.js +377 -0
  293. package/dist/core/tokens/formatters/dtcg.js.map +1 -0
  294. package/dist/core/tokens/formatters/index.d.ts +30 -0
  295. package/dist/core/tokens/formatters/index.d.ts.map +1 -0
  296. package/dist/core/tokens/formatters/index.js +46 -0
  297. package/dist/core/tokens/formatters/index.js.map +1 -0
  298. package/dist/core/tokens/formatters/json.d.ts +37 -0
  299. package/dist/core/tokens/formatters/json.d.ts.map +1 -0
  300. package/dist/core/tokens/formatters/json.js +206 -0
  301. package/dist/core/tokens/formatters/json.js.map +1 -0
  302. package/dist/core/tokens/formatters/less.d.ts +4 -0
  303. package/dist/core/tokens/formatters/less.d.ts.map +1 -0
  304. package/dist/core/tokens/formatters/less.js +5 -0
  305. package/dist/core/tokens/formatters/less.js.map +1 -0
  306. package/dist/core/tokens/formatters/scss.d.ts +26 -0
  307. package/dist/core/tokens/formatters/scss.d.ts.map +1 -0
  308. package/dist/core/tokens/formatters/scss.js +259 -0
  309. package/dist/core/tokens/formatters/scss.js.map +1 -0
  310. package/dist/core/tokens/formatters/stubs.d.ts +9 -0
  311. package/dist/core/tokens/formatters/stubs.d.ts.map +1 -0
  312. package/dist/core/tokens/formatters/stubs.js +14 -0
  313. package/dist/core/tokens/formatters/stubs.js.map +1 -0
  314. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts +45 -0
  315. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -0
  316. package/dist/core/tokens/formatters/style-dictionary-v3.js +214 -0
  317. package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -0
  318. package/dist/core/tokens/formatters/tailwind-v3.d.ts +37 -0
  319. package/dist/core/tokens/formatters/tailwind-v3.d.ts.map +1 -0
  320. package/dist/core/tokens/formatters/tailwind-v3.js +238 -0
  321. package/dist/core/tokens/formatters/tailwind-v3.js.map +1 -0
  322. package/dist/core/tokens/formatters/tailwind-v4.d.ts +41 -0
  323. package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -0
  324. package/dist/core/tokens/formatters/tailwind-v4.js +336 -0
  325. package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -0
  326. package/dist/core/tokens/formatters/tokens-studio.d.ts +44 -0
  327. package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -0
  328. package/dist/core/tokens/formatters/tokens-studio.js +257 -0
  329. package/dist/core/tokens/formatters/tokens-studio.js.map +1 -0
  330. package/dist/core/tokens/formatters/ts-module.d.ts +35 -0
  331. package/dist/core/tokens/formatters/ts-module.d.ts.map +1 -0
  332. package/dist/core/tokens/formatters/ts-module.js +199 -0
  333. package/dist/core/tokens/formatters/ts-module.js.map +1 -0
  334. package/dist/core/tokens/index.d.ts +18 -0
  335. package/dist/core/tokens/index.d.ts.map +1 -0
  336. package/dist/core/tokens/index.js +17 -0
  337. package/dist/core/tokens/index.js.map +1 -0
  338. package/dist/core/tokens/parsers/css-vars.d.ts +3 -0
  339. package/dist/core/tokens/parsers/css-vars.d.ts.map +1 -0
  340. package/dist/core/tokens/parsers/css-vars.js +5 -0
  341. package/dist/core/tokens/parsers/css-vars.js.map +1 -0
  342. package/dist/core/tokens/parsers/dtcg.d.ts +21 -0
  343. package/dist/core/tokens/parsers/dtcg.d.ts.map +1 -0
  344. package/dist/core/tokens/parsers/dtcg.js +281 -0
  345. package/dist/core/tokens/parsers/dtcg.js.map +1 -0
  346. package/dist/core/tokens/parsers/index.d.ts +37 -0
  347. package/dist/core/tokens/parsers/index.d.ts.map +1 -0
  348. package/dist/core/tokens/parsers/index.js +139 -0
  349. package/dist/core/tokens/parsers/index.js.map +1 -0
  350. package/dist/core/tokens/parsers/json.d.ts +4 -0
  351. package/dist/core/tokens/parsers/json.d.ts.map +1 -0
  352. package/dist/core/tokens/parsers/json.js +8 -0
  353. package/dist/core/tokens/parsers/json.js.map +1 -0
  354. package/dist/core/tokens/parsers/scss.d.ts +3 -0
  355. package/dist/core/tokens/parsers/scss.d.ts.map +1 -0
  356. package/dist/core/tokens/parsers/scss.js +5 -0
  357. package/dist/core/tokens/parsers/scss.js.map +1 -0
  358. package/dist/core/tokens/parsers/stubs.d.ts +15 -0
  359. package/dist/core/tokens/parsers/stubs.d.ts.map +1 -0
  360. package/dist/core/tokens/parsers/stubs.js +21 -0
  361. package/dist/core/tokens/parsers/stubs.js.map +1 -0
  362. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts +3 -0
  363. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts.map +1 -0
  364. package/dist/core/tokens/parsers/style-dictionary-v3.js +5 -0
  365. package/dist/core/tokens/parsers/style-dictionary-v3.js.map +1 -0
  366. package/dist/core/tokens/parsers/tailwind-v3.d.ts +3 -0
  367. package/dist/core/tokens/parsers/tailwind-v3.d.ts.map +1 -0
  368. package/dist/core/tokens/parsers/tailwind-v3.js +5 -0
  369. package/dist/core/tokens/parsers/tailwind-v3.js.map +1 -0
  370. package/dist/core/tokens/parsers/tailwind-v4.d.ts +3 -0
  371. package/dist/core/tokens/parsers/tailwind-v4.d.ts.map +1 -0
  372. package/dist/core/tokens/parsers/tailwind-v4.js +5 -0
  373. package/dist/core/tokens/parsers/tailwind-v4.js.map +1 -0
  374. package/dist/core/tokens/parsers/tokens-studio.d.ts +3 -0
  375. package/dist/core/tokens/parsers/tokens-studio.d.ts.map +1 -0
  376. package/dist/core/tokens/parsers/tokens-studio.js +5 -0
  377. package/dist/core/tokens/parsers/tokens-studio.js.map +1 -0
  378. package/dist/core/tokens/schemas.d.ts +155 -0
  379. package/dist/core/tokens/schemas.d.ts.map +1 -0
  380. package/dist/core/tokens/schemas.js +153 -0
  381. package/dist/core/tokens/schemas.js.map +1 -0
  382. package/dist/core/tokens/transforms/color.d.ts +9 -0
  383. package/dist/core/tokens/transforms/color.d.ts.map +1 -0
  384. package/dist/core/tokens/transforms/color.js +13 -0
  385. package/dist/core/tokens/transforms/color.js.map +1 -0
  386. package/dist/core/tokens/transforms/index.d.ts +36 -0
  387. package/dist/core/tokens/transforms/index.d.ts.map +1 -0
  388. package/dist/core/tokens/transforms/index.js +30 -0
  389. package/dist/core/tokens/transforms/index.js.map +1 -0
  390. package/dist/core/tokens/transforms/size.d.ts +7 -0
  391. package/dist/core/tokens/transforms/size.d.ts.map +1 -0
  392. package/dist/core/tokens/transforms/size.js +8 -0
  393. package/dist/core/tokens/transforms/size.js.map +1 -0
  394. package/dist/core/tokens/types.d.ts +284 -0
  395. package/dist/core/tokens/types.d.ts.map +1 -0
  396. package/dist/core/tokens/types.js +19 -0
  397. package/dist/core/tokens/types.js.map +1 -0
  398. package/dist/core/tokens-tools.d.ts +285 -0
  399. package/dist/core/tokens-tools.d.ts.map +1 -0
  400. package/dist/core/tokens-tools.js +1789 -0
  401. package/dist/core/tokens-tools.js.map +1 -0
  402. package/dist/core/types/design-code.d.ts +271 -0
  403. package/dist/core/types/design-code.d.ts.map +1 -0
  404. package/dist/core/types/design-code.js +5 -0
  405. package/dist/core/types/design-code.js.map +1 -0
  406. package/dist/core/types/enriched.d.ts +213 -0
  407. package/dist/core/types/enriched.d.ts.map +1 -0
  408. package/dist/core/types/enriched.js +6 -0
  409. package/dist/core/types/enriched.js.map +1 -0
  410. package/dist/core/types/index.d.ts +104 -0
  411. package/dist/core/types/index.d.ts.map +1 -0
  412. package/dist/core/types/index.js +5 -0
  413. package/dist/core/types/index.js.map +1 -0
  414. package/dist/core/variable-resolver.d.ts +45 -0
  415. package/dist/core/variable-resolver.d.ts.map +1 -0
  416. package/dist/core/variable-resolver.js +86 -0
  417. package/dist/core/variable-resolver.js.map +1 -0
  418. package/dist/core/version-tools.d.ts +59 -0
  419. package/dist/core/version-tools.d.ts.map +1 -0
  420. package/dist/core/version-tools.js +1200 -0
  421. package/dist/core/version-tools.js.map +1 -0
  422. package/dist/core/websocket-connector.d.ts +247 -0
  423. package/dist/core/websocket-connector.d.ts.map +1 -0
  424. package/dist/core/websocket-connector.js +437 -0
  425. package/dist/core/websocket-connector.js.map +1 -0
  426. package/dist/core/websocket-server.d.ts +290 -0
  427. package/dist/core/websocket-server.d.ts.map +1 -0
  428. package/dist/core/websocket-server.js +957 -0
  429. package/dist/core/websocket-server.js.map +1 -0
  430. package/dist/core/write-tools.d.ts +7 -0
  431. package/dist/core/write-tools.d.ts.map +1 -0
  432. package/dist/core/write-tools.js +2507 -0
  433. package/dist/core/write-tools.js.map +1 -0
  434. package/dist/local.d.ts +105 -0
  435. package/dist/local.d.ts.map +1 -0
  436. package/dist/local.js +3314 -0
  437. package/dist/local.js.map +1 -0
  438. package/figma-desktop-bridge/README.md +365 -0
  439. package/figma-desktop-bridge/code.js +7359 -0
  440. package/figma-desktop-bridge/icon.png +0 -0
  441. package/figma-desktop-bridge/manifest.json +67 -0
  442. package/figma-desktop-bridge/ui.html +2783 -0
  443. package/package.json +103 -0
@@ -0,0 +1,2783 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <style>
6
+ * {
7
+ box-sizing: border-box;
8
+ margin: 0;
9
+ padding: 0;
10
+ }
11
+
12
+ /* ===== Status + log colour tokens ===== */
13
+ /* Dark defaults — body[data-theme] overrides below keep light/dark correct. */
14
+ :root {
15
+ --color-connected: #44FF88;
16
+ --color-connected-glow: rgba(68, 255, 136, 0.5);
17
+ --color-waiting: #FFB700;
18
+ --color-error: #FF455B;
19
+ --color-idle: #737373;
20
+ --log-info: #6cf;
21
+ --log-success: #6f6;
22
+ --log-error: #ff8080;
23
+ --log-warn: #fc0;
24
+ }
25
+
26
+ body {
27
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
28
+ font-size: 11px;
29
+ background: var(--figma-color-bg, #2c2c2c);
30
+ color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
31
+ padding: 4px 12px;
32
+ user-select: none;
33
+ }
34
+
35
+ /* Visible keyboard focus for all interactive elements (WCAG 2.4.7) */
36
+ button:focus-visible,
37
+ input:focus-visible {
38
+ outline: 2px solid var(--figma-color-bg-brand, #0d99ff);
39
+ outline-offset: 1px;
40
+ border-radius: 3px;
41
+ }
42
+ /* Hide outline when mouse-clicking but keep it for keyboard nav */
43
+ button:focus:not(:focus-visible),
44
+ input:focus:not(:focus-visible) {
45
+ outline: none;
46
+ }
47
+
48
+ .wrap {
49
+ display: flex;
50
+ flex-direction: column;
51
+ gap: 4px;
52
+ width: 100%;
53
+ }
54
+
55
+ /* ===== Row 1 — status + CTA + icons (always visible) ===== */
56
+ .row-top {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 6px;
60
+ white-space: nowrap;
61
+ padding-left: 4px; /* visual offset to align with Figma title bar icon */
62
+ }
63
+
64
+ .status-pill {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 5px;
68
+ margin-right: 3px;
69
+ /* Take the free space so long status sentences ellipsize gracefully
70
+ instead of pushing the CTA/icons off the 240px canvas. */
71
+ flex: 1;
72
+ min-width: 0;
73
+ }
74
+
75
+ .status-indicator {
76
+ width: 8px;
77
+ height: 8px;
78
+ border-radius: 50%;
79
+ flex-shrink: 0;
80
+ background: var(--color-idle);
81
+ }
82
+
83
+ .status-indicator.loading {
84
+ background: var(--color-waiting);
85
+ animation: pulse 1.5s ease-in-out infinite;
86
+ }
87
+
88
+ .status-indicator.active {
89
+ background: var(--color-connected);
90
+ box-shadow: 0 0 6px var(--color-connected-glow);
91
+ }
92
+
93
+ .status-indicator.error {
94
+ background: var(--color-error);
95
+ }
96
+
97
+ /* Deliberate user pause — static grey dot, NOT the pulsing loading
98
+ animation, so a pause never reads as "trying to connect". */
99
+ .status-indicator.paused {
100
+ background: var(--color-idle);
101
+ }
102
+
103
+ @keyframes pulse {
104
+ 0%, 100% { opacity: 0.4; }
105
+ 50% { opacity: 1; }
106
+ }
107
+
108
+ #status-state {
109
+ font-weight: 600;
110
+ font-size: 10px;
111
+ letter-spacing: -0.2px;
112
+ font-stretch: 75%;
113
+ color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.6));
114
+ overflow: hidden;
115
+ text-overflow: ellipsis;
116
+ white-space: nowrap;
117
+ min-width: 0;
118
+ }
119
+
120
+ .status-indicator.active + #status-state {
121
+ color: var(--color-connected);
122
+ }
123
+
124
+ .status-indicator.error + #status-state {
125
+ color: var(--color-error);
126
+ }
127
+
128
+ .cta-btn {
129
+ background: transparent;
130
+ color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
131
+ border: 1px solid var(--figma-color-border, #4a4a4a);
132
+ border-radius: 3px;
133
+ font-family: inherit;
134
+ font-size: 10px;
135
+ font-weight: 500;
136
+ padding: 4px;
137
+ cursor: pointer;
138
+ line-height: 1.4;
139
+ }
140
+
141
+ .cta-btn:disabled {
142
+ opacity: 0.6;
143
+ cursor: default;
144
+ }
145
+
146
+ .cta-btn:hover {
147
+ background: var(--figma-color-bg-secondary, #383838);
148
+ }
149
+
150
+ /* ===== Status meta line — connection count + proof of life ===== */
151
+ .status-meta {
152
+ display: none; /* toggled to flex from JS when there's something to show */
153
+ align-items: center;
154
+ gap: 8px;
155
+ padding-left: 17px; /* aligns with the pill text (4px row offset + 8px dot + 5px gap) */
156
+ font-size: 9px;
157
+ color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.5));
158
+ white-space: nowrap;
159
+ }
160
+
161
+ /* Visible recovery instruction for the ERROR state */
162
+ .status-error-hint {
163
+ display: none; /* shown from JS while in the error state */
164
+ padding-left: 17px;
165
+ font-size: 10px;
166
+ line-height: 1.4;
167
+ color: var(--color-error);
168
+ }
169
+
170
+ .icon-btn {
171
+ background: transparent;
172
+ border: 1px solid transparent;
173
+ border-radius: 3px;
174
+ color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.6));
175
+ width: 24px;
176
+ height: 24px;
177
+ padding: 0;
178
+ display: inline-flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ cursor: pointer;
182
+ font-family: inherit;
183
+ font-size: 14px;
184
+ line-height: 1;
185
+ }
186
+
187
+ .icon-btn:hover {
188
+ color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
189
+ border-color: var(--figma-color-border, #4a4a4a);
190
+ }
191
+
192
+ /* Borderless icon variant — hover/active change colour only, never border.
193
+ Applied via .icon-btn--borderless modifier class. */
194
+ .icon-btn--borderless,
195
+ .icon-btn--borderless:hover,
196
+ .icon-btn--borderless.active {
197
+ border-color: transparent !important;
198
+ }
199
+
200
+ /* Expand [+] / [−] glyph is text, not SVG. Bump to match visual weight of SVG siblings. */
201
+ #expand-btn {
202
+ font-size: 18px;
203
+ font-weight: 400;
204
+ line-height: 1;
205
+ }
206
+
207
+ .icon-btn.active,
208
+ .icon-btn.active:hover {
209
+ color: var(--figma-color-bg-brand, #0d99ff);
210
+ border-color: var(--figma-color-bg-brand, #0d99ff);
211
+ }
212
+
213
+ .icon-btn svg {
214
+ width: 14px;
215
+ height: 14px;
216
+ }
217
+
218
+ /* ===== Row: cloud pairing (when cloud icon on) ===== */
219
+ .row {
220
+ display: none;
221
+ width: 100%;
222
+ }
223
+
224
+ .row.visible {
225
+ display: flex;
226
+ }
227
+
228
+ .cloud-pair {
229
+ flex-direction: row;
230
+ gap: 4px;
231
+ align-items: stretch;
232
+ }
233
+
234
+ .cloud-pair input {
235
+ flex: 1;
236
+ min-width: 0;
237
+ background: var(--figma-color-bg, #2c2c2c);
238
+ border: 1px solid var(--figma-color-border, #4a4a4a);
239
+ border-radius: 3px;
240
+ color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
241
+ font-family: monospace;
242
+ font-size: 11px;
243
+ padding: 3px 5px;
244
+ text-transform: uppercase;
245
+ letter-spacing: 2px;
246
+ text-align: center;
247
+ box-sizing: border-box;
248
+ }
249
+
250
+ .cloud-pair input::placeholder {
251
+ text-transform: none;
252
+ letter-spacing: normal;
253
+ font-family: inherit;
254
+ font-size: 10px;
255
+ color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.3));
256
+ }
257
+
258
+ /* Connect button only; icon-btn inside the row keeps its icon-btn styling */
259
+ .cloud-pair button:not(.icon-btn) {
260
+ flex-shrink: 0;
261
+ background: var(--figma-color-bg-brand, #0d99ff);
262
+ color: #fff;
263
+ border: none;
264
+ border-radius: 3px;
265
+ font-family: inherit;
266
+ font-size: 10px;
267
+ font-weight: 500;
268
+ padding: 4px 10px;
269
+ cursor: pointer;
270
+ }
271
+
272
+ .cloud-pair button:not(.icon-btn):disabled {
273
+ opacity: 0.5;
274
+ cursor: default;
275
+ }
276
+
277
+ .cloud-help {
278
+ flex-direction: column;
279
+ gap: 4px;
280
+ padding: 6px 8px;
281
+ background: var(--figma-color-bg-secondary, #383838);
282
+ border-radius: 3px;
283
+ font-size: 10px;
284
+ line-height: 1.4;
285
+ color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.7));
286
+ }
287
+
288
+ .cloud-help p {
289
+ margin: 0;
290
+ }
291
+
292
+ body[data-theme="light"] .cloud-help {
293
+ background: #f0f0f0;
294
+ color: #555;
295
+ }
296
+
297
+ .cloud-status {
298
+ display: none;
299
+ font-size: 9px;
300
+ color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.5));
301
+ text-align: center;
302
+ padding: 2px 0;
303
+ }
304
+
305
+ .cloud-status:not(:empty) {
306
+ display: block;
307
+ }
308
+
309
+ .cloud-status.connected {
310
+ color: var(--color-connected);
311
+ }
312
+
313
+ .cloud-status.error {
314
+ color: var(--color-error);
315
+ }
316
+
317
+ /* ===== Row: sub-toolbar (when [+] on) ===== */
318
+ .sub-toolbar {
319
+ align-items: center;
320
+ gap: 8px;
321
+ flex-wrap: nowrap;
322
+ padding-left: 4px; /* match row-top alignment with Figma title bar icon */
323
+ }
324
+
325
+ .sub-btn {
326
+ background: transparent;
327
+ border: none;
328
+ padding: 2px 0;
329
+ color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.6));
330
+ font-family: inherit;
331
+ font-size: 10px;
332
+ cursor: pointer;
333
+ display: inline-flex;
334
+ align-items: center;
335
+ gap: 3px;
336
+ white-space: nowrap;
337
+ }
338
+
339
+ .sub-btn:hover {
340
+ color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
341
+ }
342
+
343
+ .sub-btn.active {
344
+ color: var(--figma-color-bg-brand, #0d99ff);
345
+ }
346
+
347
+ .sub-btn svg {
348
+ width: 11px;
349
+ height: 11px;
350
+ }
351
+
352
+ /* ===== Row: plugin update banner ===== */
353
+ .update-banner {
354
+ align-items: center;
355
+ gap: 6px;
356
+ padding: 4px 6px;
357
+ background: var(--figma-color-bg-secondary, #383838);
358
+ border: 1px solid var(--figma-color-border, #4a4a4a);
359
+ border-radius: 3px;
360
+ font-size: 10px;
361
+ line-height: 1.4;
362
+ color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
363
+ }
364
+
365
+ .update-banner span {
366
+ flex: 1;
367
+ min-width: 0;
368
+ }
369
+
370
+ /* ===== Row: log panel ===== */
371
+ .log-panel {
372
+ flex-direction: column;
373
+ gap: 0;
374
+ border: 1px solid var(--figma-color-border, #4a4a4a);
375
+ border-radius: 3px;
376
+ overflow: hidden;
377
+ background: var(--figma-color-bg, #1e1e1e);
378
+ }
379
+
380
+ .log-entries {
381
+ max-height: 160px;
382
+ overflow-y: auto;
383
+ padding: 4px 6px;
384
+ font-family: 'SF Mono', 'Menlo', Consolas, monospace;
385
+ font-size: 10px;
386
+ line-height: 1.4;
387
+ }
388
+
389
+ .log-entry {
390
+ display: flex;
391
+ align-items: baseline;
392
+ gap: 5px;
393
+ color: var(--figma-color-text, rgba(255, 255, 255, 0.8));
394
+ line-height: 1.5;
395
+ }
396
+ .log-ts {
397
+ flex-shrink: 0;
398
+ opacity: 0.45;
399
+ font-size: 9px;
400
+ user-select: none;
401
+ }
402
+ .log-msg {
403
+ flex: 1;
404
+ min-width: 0;
405
+ overflow: hidden;
406
+ text-overflow: ellipsis;
407
+ white-space: nowrap;
408
+ }
409
+ .log-dur {
410
+ display: none;
411
+ }
412
+ .log-count {
413
+ flex-shrink: 0;
414
+ opacity: 0.5;
415
+ font-size: 9px;
416
+ min-width: 18px;
417
+ text-align: right;
418
+ }
419
+
420
+ .log-entry.info { color: var(--log-info); }
421
+ .log-entry.success{ color: var(--log-success); }
422
+ .log-entry.error { color: var(--log-error); }
423
+ .log-entry.warn { color: var(--log-warn); }
424
+
425
+ /* ===== Light theme ===== */
426
+ /* Theme manual override redefines the Figma-injected vars so all token
427
+ consumers cascade automatically. When no data-theme attribute is set,
428
+ Figma's themeColors:true (in code.js) controls the values natively. */
429
+ body[data-theme="light"] {
430
+ --figma-color-bg: #ffffff;
431
+ --figma-color-bg-secondary: #f5f5f5;
432
+ --figma-color-border: #e5e5e5;
433
+ --figma-color-text: #333333;
434
+ --figma-color-text-secondary: #777777;
435
+ --color-connected: #16a34a;
436
+ --color-connected-glow: rgba(22, 163, 74, 0.45);
437
+ --color-waiting: #d97706;
438
+ --color-error: #ef4444;
439
+ --color-idle: #6b7280;
440
+ --log-info: #00639e;
441
+ --log-success: #167016;
442
+ --log-error: #b81e2c;
443
+ --log-warn: #7a5c00;
444
+ }
445
+ body[data-theme="dark"] {
446
+ --figma-color-bg: #2c2c2c;
447
+ --figma-color-bg-secondary: #383838;
448
+ --figma-color-border: #4a4a4a;
449
+ --figma-color-text: rgba(255, 255, 255, 0.9);
450
+ --figma-color-text-secondary: rgba(255, 255, 255, 0.55);
451
+ --color-connected: #44FF88;
452
+ --color-connected-glow: rgba(68, 255, 136, 0.5);
453
+ --color-waiting: #FFB700;
454
+ --color-error: #FF455B;
455
+ --color-idle: #737373;
456
+ --log-info: #6cf;
457
+ --log-success: #6f6;
458
+ --log-error: #ff8080;
459
+ --log-warn: #fc0;
460
+ }
461
+ </style>
462
+ </head>
463
+ <body>
464
+ <div class="wrap">
465
+ <!-- Row 1 — always visible -->
466
+ <div class="row-top">
467
+ <div class="status-pill">
468
+ <div class="status-indicator loading" id="status-dot" aria-hidden="true"></div>
469
+ <span id="status-state" role="status" aria-live="polite">Looking for your AI app…</span>
470
+ </div>
471
+ <!-- CTA starts hidden; the first state reconcile shows it with an honest label -->
472
+ <button class="cta-btn" id="cta-btn" onclick="toggleLocalConnection()" style="display:none">Pause</button>
473
+ <button class="icon-btn" id="cloud-icon" onclick="toggleCloudPair()" title="Cloud pairing" aria-label="Cloud pairing" aria-expanded="false">
474
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
475
+ <path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>
476
+ </svg>
477
+ </button>
478
+ <button class="icon-btn" id="expand-btn" onclick="toggleSubToolbar()" title="Show options" aria-label="Show options" aria-expanded="false">+</button>
479
+ </div>
480
+
481
+ <!-- Status meta — connection count + proof of life (shown when connected) -->
482
+ <div class="status-meta" id="status-meta">
483
+ <span id="status-conn-count"></span>
484
+ <span id="status-last-action"></span>
485
+ </div>
486
+
487
+ <!-- Visible recovery instruction, shown only in the error state -->
488
+ <div class="status-error-hint" id="status-error-hint">Close this window and reopen the plugin.</div>
489
+
490
+ <!-- Plugin update banner (shown when the server reports a newer plugin) -->
491
+ <div class="row update-banner" id="update-banner">
492
+ <span>Plugin update available — re-import it in Figma (Plugins → Development → Import from manifest).</span>
493
+ <button class="icon-btn icon-btn--borderless" onclick="dismissUpdateBanner()" title="Dismiss" aria-label="Dismiss update notice">×</button>
494
+ </div>
495
+
496
+ <!-- Cloud pairing (shown when cloud icon active) -->
497
+ <div class="row cloud-pair" id="cloud-pair">
498
+ <button class="icon-btn icon-btn--borderless cloud-info-btn" id="cloud-info-btn" onclick="toggleCloudHelp()" title="About pairing codes" aria-label="About pairing codes" aria-expanded="false" aria-controls="cloud-help">
499
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
500
+ <circle cx="12" cy="12" r="10"/>
501
+ <line x1="12" y1="16" x2="12" y2="12"/>
502
+ <line x1="12" y1="8" x2="12.01" y2="8"/>
503
+ </svg>
504
+ </button>
505
+ <input type="text" id="cloud-code" maxlength="6" placeholder="Pairing code" autocomplete="off" aria-label="Cloud pairing code" />
506
+ <button id="cloud-btn" onclick="cloudConnect()">Connect</button>
507
+ </div>
508
+ <div class="row cloud-help" id="cloud-help" role="region" aria-label="About pairing codes">
509
+ <p>Use this when Claude is on a different device from Figma.</p>
510
+ <p>Generate a 6-char code in Claude and paste it here.</p>
511
+ </div>
512
+ <div class="cloud-status" id="cloud-status"></div>
513
+
514
+ <!-- Sub-toolbar (shown when [+] active) -->
515
+ <div class="row sub-toolbar" id="sub-toolbar">
516
+ <button class="sub-btn" id="log-toggle" onclick="toggleLog()" aria-expanded="false" aria-controls="log-panel">
517
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
518
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
519
+ <circle cx="12" cy="12" r="3"/>
520
+ </svg>
521
+ Activity
522
+ </button>
523
+ <button class="icon-btn" id="copy-log-btn" onclick="copyLogToClipboard()" title="Copy activity to clipboard" aria-label="Copy activity to clipboard" style="display:none">
524
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
525
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
526
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
527
+ </svg>
528
+ </button>
529
+ </div>
530
+
531
+ <!-- Activity panel (shown when Activity active) -->
532
+ <div class="row log-panel" id="log-panel">
533
+ <div class="log-entries" id="log-entries"></div>
534
+ </div>
535
+ </div>
536
+
537
+ <script>
538
+ // ============================================================================
539
+ // GLOBAL STATE — backing store for the in-iframe WebSocket bridge client
540
+ // ============================================================================
541
+ window.__figmaVariablesData = null;
542
+ window.__figmaVariablesReady = false;
543
+ window.__figmaComponentData = null;
544
+ window.__figmaComponentRequests = new Map();
545
+ window.__figmaPendingRequests = new Map();
546
+
547
+ let requestIdCounter = 0;
548
+
549
+ // Status inputs owned by the message handler at the bottom of this file.
550
+ // bridgeError flips true on a code.js ERROR message; a later successful
551
+ // VARIABLES_DATA clears it (the plugin recovered).
552
+ var bridgeError = false;
553
+
554
+ // Single source of truth for the status pill. Derives the rendered state
555
+ // from actual facts instead of trusting one-off events:
556
+ // - live WS connections (local + cloud) from the connection pool
557
+ // - userPaused (pool exposes __wsIsPaused)
558
+ // - scanning/discovery activity (__wsIsScanning / __wsIsDiscoveryActive)
559
+ // - bridgeError (ERROR message from code.js)
560
+ // window.__figmaVariablesReady is also an input (set by VARIABLES_DATA);
561
+ // it no longer drives the pill green on its own — a Figma-side data event
562
+ // says nothing about whether an AI app is actually connected.
563
+ function deriveAndRenderStatus() {
564
+ var dot = document.getElementById('status-dot');
565
+ var stateText = document.getElementById('status-state');
566
+ if (!dot || !stateText) return;
567
+
568
+ var conns = (typeof window.__wsGetActiveConnections === 'function') ? (window.__wsGetActiveConnections() || []) : [];
569
+ var live = 0;
570
+ for (var i = 0; i < conns.length; i++) {
571
+ if (conns[i] && conns[i].ws && conns[i].ws.readyState === 1) live++;
572
+ }
573
+ var paused = (typeof window.__wsIsPaused === 'function') ? window.__wsIsPaused() : false;
574
+ var scanning = (typeof window.__wsIsScanning === 'function' && window.__wsIsScanning()) ||
575
+ (typeof window.__wsIsDiscoveryActive === 'function' && window.__wsIsDiscoveryActive());
576
+
577
+ var dotClass, text;
578
+ if (bridgeError) {
579
+ dotClass = 'error';
580
+ text = 'Something broke';
581
+ } else if (paused) {
582
+ dotClass = 'paused';
583
+ text = 'Paused — AI can\'t make changes';
584
+ } else if (live >= 1) {
585
+ dotClass = 'active';
586
+ text = 'Connected — AI can work in this file';
587
+ } else if (scanning) {
588
+ dotClass = 'loading';
589
+ text = 'Looking for your AI app…';
590
+ } else {
591
+ // Defensive fallback: discovery is always on unless paused, so this
592
+ // only renders if the discovery loop is somehow disabled.
593
+ dotClass = '';
594
+ text = 'Not connected';
595
+ }
596
+ dot.className = 'status-indicator' + (dotClass ? ' ' + dotClass : '');
597
+ stateText.textContent = text;
598
+ stateText.title = text; // full sentence survives ellipsis truncation
599
+
600
+ var hint = document.getElementById('status-error-hint');
601
+ if (hint) hint.style.display = bridgeError ? 'block' : 'none';
602
+
603
+ renderStatusMeta(live);
604
+ }
605
+
606
+ // Small always-visible line under the pill: "Connected to N AI app(s)"
607
+ // plus proof of life ("Last action: Xs ago"). Hidden entirely when there
608
+ // is nothing to show (zero connections).
609
+ function renderStatusMeta(liveCount) {
610
+ var meta = document.getElementById('status-meta');
611
+ var countEl = document.getElementById('status-conn-count');
612
+ var lastEl = document.getElementById('status-last-action');
613
+ if (!meta || !countEl || !lastEl) return;
614
+ var anything = false;
615
+ if (liveCount >= 1) {
616
+ countEl.textContent = 'Connected to ' + liveCount + ' AI app' + (liveCount === 1 ? '' : 's');
617
+ countEl.style.display = '';
618
+ anything = true;
619
+ } else {
620
+ countEl.textContent = '';
621
+ countEl.style.display = 'none';
622
+ }
623
+ if (liveCount >= 1 && _lastActionAt) {
624
+ lastEl.textContent = 'Last action: ' + formatAgo(Date.now() - _lastActionAt);
625
+ lastEl.style.display = '';
626
+ anything = true;
627
+ } else {
628
+ lastEl.textContent = '';
629
+ lastEl.style.display = 'none';
630
+ }
631
+ meta.style.display = anything ? 'flex' : 'none';
632
+ }
633
+
634
+ function formatAgo(ms) {
635
+ var s = Math.max(0, Math.floor(ms / 1000));
636
+ if (s < 60) return s + 's ago';
637
+ var m = Math.floor(s / 60);
638
+ if (m < 60) return m + 'm ago';
639
+ var h = Math.floor(m / 60);
640
+ return h + 'h ago';
641
+ }
642
+
643
+ // ============================================================================
644
+ // COMMAND INFRASTRUCTURE - Generic plugin command sender
645
+ // ============================================================================
646
+ window.sendPluginCommand = (type, params, timeoutMs) => {
647
+ timeoutMs = timeoutMs || 15000;
648
+ return new Promise((resolve, reject) => {
649
+ const requestId = type.toLowerCase() + '_' + (++requestIdCounter) + '_' + Date.now();
650
+
651
+ const timeoutId = setTimeout(() => {
652
+ if (window.__figmaPendingRequests.has(requestId)) {
653
+ window.__figmaPendingRequests.delete(requestId);
654
+ reject(new Error(type + ' request timed out after ' + timeoutMs + 'ms'));
655
+ }
656
+ }, timeoutMs);
657
+
658
+ window.__figmaPendingRequests.set(requestId, { resolve: resolve, reject: reject, type: type, timeoutId: timeoutId });
659
+
660
+ var message = { type: type, requestId: requestId };
661
+ for (var key in params) {
662
+ if (params.hasOwnProperty(key)) {
663
+ message[key] = params[key];
664
+ }
665
+ }
666
+
667
+ parent.postMessage({ pluginMessage: message }, '*');
668
+ console.log('[MCP Bridge] Sent:', type);
669
+ });
670
+ };
671
+
672
+ // ============================================================================
673
+ // VARIABLE OPERATIONS
674
+ // ============================================================================
675
+
676
+ window.executeCode = (code, timeout) => {
677
+ return window.sendPluginCommand('EXECUTE_CODE', { code: code, timeout: timeout || 5000 }, (timeout || 5000) + 2000)
678
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
679
+ };
680
+
681
+ window.updateVariable = (variableId, modeId, value) => {
682
+ return window.sendPluginCommand('UPDATE_VARIABLE', { variableId: variableId, modeId: modeId, value: value })
683
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
684
+ };
685
+
686
+ window.createVariable = (name, collectionId, resolvedType, options) => {
687
+ var params = { name: name, collectionId: collectionId, resolvedType: resolvedType };
688
+ if (options) {
689
+ if (options.valuesByMode) params.valuesByMode = options.valuesByMode;
690
+ if (options.description) params.description = options.description;
691
+ if (options.scopes) params.scopes = options.scopes;
692
+ }
693
+ return window.sendPluginCommand('CREATE_VARIABLE', params)
694
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
695
+ };
696
+
697
+ window.createVariableCollection = (name, options) => {
698
+ var params = { name: name };
699
+ if (options) {
700
+ if (options.initialModeName) params.initialModeName = options.initialModeName;
701
+ if (options.additionalModes) params.additionalModes = options.additionalModes;
702
+ }
703
+ return window.sendPluginCommand('CREATE_VARIABLE_COLLECTION', params)
704
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
705
+ };
706
+
707
+ window.deleteVariable = (variableId) => {
708
+ return window.sendPluginCommand('DELETE_VARIABLE', { variableId: variableId })
709
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
710
+ };
711
+
712
+ window.deleteVariableCollection = (collectionId) => {
713
+ return window.sendPluginCommand('DELETE_VARIABLE_COLLECTION', { collectionId: collectionId })
714
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
715
+ };
716
+
717
+ window.renameVariable = (variableId, newName) => {
718
+ return window.sendPluginCommand('RENAME_VARIABLE', { variableId: variableId, newName: newName })
719
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
720
+ };
721
+
722
+ window.setVariableDescription = (variableId, description) => {
723
+ return window.sendPluginCommand('SET_VARIABLE_DESCRIPTION', { variableId: variableId, description: description })
724
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
725
+ };
726
+
727
+ window.addMode = (collectionId, modeName) => {
728
+ return window.sendPluginCommand('ADD_MODE', { collectionId: collectionId, modeName: modeName })
729
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
730
+ };
731
+
732
+ window.renameMode = (collectionId, modeId, newName) => {
733
+ return window.sendPluginCommand('RENAME_MODE', { collectionId: collectionId, modeId: modeId, newName: newName })
734
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
735
+ };
736
+
737
+ window.refreshVariables = () => {
738
+ return window.sendPluginCommand('REFRESH_VARIABLES', {}, 300000)
739
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
740
+ };
741
+
742
+ // ============================================================================
743
+ // COMPONENT OPERATIONS
744
+ // ============================================================================
745
+
746
+ window.getLocalComponents = () => {
747
+ return window.sendPluginCommand('GET_LOCAL_COMPONENTS', {}, 300000)
748
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
749
+ };
750
+
751
+ window.instantiateComponent = (componentKey, options) => {
752
+ var params = { componentKey: componentKey };
753
+ if (options) {
754
+ if (options.nodeId) params.nodeId = options.nodeId;
755
+ if (options.position) params.position = options.position;
756
+ if (options.size) params.size = options.size;
757
+ if (options.overrides) params.overrides = options.overrides;
758
+ if (options.variant) params.variant = options.variant;
759
+ if (options.parentId) params.parentId = options.parentId;
760
+ }
761
+ return window.sendPluginCommand('INSTANTIATE_COMPONENT', params)
762
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
763
+ };
764
+
765
+ window.requestComponentData = (nodeId) => {
766
+ return new Promise((resolve, reject) => {
767
+ const requestId = 'component_' + (++requestIdCounter) + '_' + Date.now();
768
+ window.__figmaComponentRequests.set(requestId, { resolve: resolve, reject: reject });
769
+ parent.postMessage({ pluginMessage: { type: 'GET_COMPONENT', requestId: requestId, nodeId: nodeId } }, '*');
770
+ setTimeout(() => {
771
+ if (window.__figmaComponentRequests.has(requestId)) {
772
+ window.__figmaComponentRequests.delete(requestId);
773
+ reject(new Error('Component request timed out'));
774
+ }
775
+ }, 10000);
776
+ });
777
+ };
778
+
779
+ // ============================================================================
780
+ // NEW: COMPONENT PROPERTY MANAGEMENT
781
+ // ============================================================================
782
+
783
+ // Set component/node description
784
+ window.setNodeDescription = (nodeId, description, descriptionMarkdown) => {
785
+ return window.sendPluginCommand('SET_NODE_DESCRIPTION', {
786
+ nodeId: nodeId,
787
+ description: description,
788
+ descriptionMarkdown: descriptionMarkdown
789
+ }).catch(function(err) { return { success: false, error: err.message || String(err) }; });
790
+ };
791
+
792
+ // Add a component property (BOOLEAN, TEXT, INSTANCE_SWAP, VARIANT, SLOT)
793
+ // Note: We use 'propertyType' instead of 'type' to avoid collision with message type field
794
+ window.addComponentProperty = (nodeId, propertyName, type, defaultValue, options) => {
795
+ var params = { nodeId: nodeId, propertyName: propertyName, propertyType: type, defaultValue: defaultValue };
796
+ if (options) {
797
+ if (options.preferredValues) params.preferredValues = options.preferredValues;
798
+ if (options.description) params.description = options.description;
799
+ }
800
+ return window.sendPluginCommand('ADD_COMPONENT_PROPERTY', params)
801
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
802
+ };
803
+
804
+ // ============================================================================
805
+ // SLOT OPERATIONS
806
+ // ============================================================================
807
+
808
+ window.createSlot = (nodeId, options) => {
809
+ return window.sendPluginCommand('CREATE_SLOT', Object.assign({ nodeId: nodeId }, options || {}))
810
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
811
+ };
812
+
813
+ window.getSlots = (nodeId) => {
814
+ return window.sendPluginCommand('GET_SLOTS', { nodeId: nodeId })
815
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
816
+ };
817
+
818
+ window.appendToSlot = (params) => {
819
+ return window.sendPluginCommand('APPEND_TO_SLOT', params)
820
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
821
+ };
822
+
823
+ window.resetSlot = (params) => {
824
+ return window.sendPluginCommand('RESET_SLOT', params)
825
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
826
+ };
827
+
828
+ // Edit an existing component property
829
+ window.editComponentProperty = (nodeId, propertyName, newValue) => {
830
+ return window.sendPluginCommand('EDIT_COMPONENT_PROPERTY', {
831
+ nodeId: nodeId,
832
+ propertyName: propertyName,
833
+ newValue: newValue
834
+ }).catch(function(err) { return { success: false, error: err.message || String(err) }; });
835
+ };
836
+
837
+ // Delete a component property
838
+ window.deleteComponentProperty = (nodeId, propertyName) => {
839
+ return window.sendPluginCommand('DELETE_COMPONENT_PROPERTY', {
840
+ nodeId: nodeId,
841
+ propertyName: propertyName
842
+ }).catch(function(err) { return { success: false, error: err.message || String(err) }; });
843
+ };
844
+
845
+ // ============================================================================
846
+ // NEW: NODE MANIPULATION
847
+ // ============================================================================
848
+
849
+ // Resize any node
850
+ window.resizeNode = (nodeId, width, height, withConstraints) => {
851
+ return window.sendPluginCommand('RESIZE_NODE', {
852
+ nodeId: nodeId,
853
+ width: width,
854
+ height: height,
855
+ withConstraints: withConstraints !== false
856
+ }).catch(function(err) { return { success: false, error: err.message || String(err) }; });
857
+ };
858
+
859
+ // Move/position a node
860
+ window.moveNode = (nodeId, x, y) => {
861
+ return window.sendPluginCommand('MOVE_NODE', { nodeId: nodeId, x: x, y: y })
862
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
863
+ };
864
+
865
+ // Set node fills (colors)
866
+ window.setNodeFills = (nodeId, fills) => {
867
+ return window.sendPluginCommand('SET_NODE_FILLS', { nodeId: nodeId, fills: fills })
868
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
869
+ };
870
+
871
+ // Set node strokes
872
+ window.setNodeStrokes = (nodeId, strokes, strokeWeight) => {
873
+ var params = { nodeId: nodeId, strokes: strokes };
874
+ if (strokeWeight !== undefined) params.strokeWeight = strokeWeight;
875
+ return window.sendPluginCommand('SET_NODE_STROKES', params)
876
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
877
+ };
878
+
879
+ // Set node opacity
880
+ window.setNodeOpacity = (nodeId, opacity) => {
881
+ return window.sendPluginCommand('SET_NODE_OPACITY', { nodeId: nodeId, opacity: opacity })
882
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
883
+ };
884
+
885
+ // Set node corner radius
886
+ window.setNodeCornerRadius = (nodeId, radius) => {
887
+ return window.sendPluginCommand('SET_NODE_CORNER_RADIUS', { nodeId: nodeId, radius: radius })
888
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
889
+ };
890
+
891
+ // Clone a node
892
+ window.cloneNode = (nodeId) => {
893
+ return window.sendPluginCommand('CLONE_NODE', { nodeId: nodeId })
894
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
895
+ };
896
+
897
+ // Delete a node
898
+ window.deleteNode = (nodeId) => {
899
+ return window.sendPluginCommand('DELETE_NODE', { nodeId: nodeId })
900
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
901
+ };
902
+
903
+ // Rename a node
904
+ window.renameNode = (nodeId, newName) => {
905
+ return window.sendPluginCommand('RENAME_NODE', { nodeId: nodeId, newName: newName })
906
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
907
+ };
908
+
909
+ // Set text content (for text nodes)
910
+ window.setTextContent = (nodeId, text, options) => {
911
+ var params = { nodeId: nodeId, text: text };
912
+ if (options) {
913
+ if (options.fontSize) params.fontSize = options.fontSize;
914
+ if (options.fontWeight) params.fontWeight = options.fontWeight;
915
+ if (options.fontFamily) params.fontFamily = options.fontFamily;
916
+ if (options.fontStyle) params.fontStyle = options.fontStyle;
917
+ }
918
+ return window.sendPluginCommand('SET_TEXT_CONTENT', params)
919
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
920
+ };
921
+
922
+ // Create a new node as child
923
+ window.createChildNode = (parentId, nodeType, properties) => {
924
+ return window.sendPluginCommand('CREATE_CHILD_NODE', {
925
+ parentId: parentId,
926
+ nodeType: nodeType,
927
+ properties: properties || {}
928
+ }).catch(function(err) { return { success: false, error: err.message || String(err) }; });
929
+ };
930
+
931
+ // ============================================================================
932
+ // NEW: SCREENSHOT & INSTANCE PROPERTIES (Fix for visual validation loop)
933
+ // ============================================================================
934
+
935
+ // Capture screenshot using plugin's exportAsync (reads current plugin state, not cloud)
936
+ // This solves the race condition where REST API screenshots show stale state
937
+ window.captureScreenshot = (nodeId, options) => {
938
+ var params = { nodeId: nodeId };
939
+ if (options) {
940
+ if (options.format) params.format = options.format; // PNG, JPG, SVG
941
+ if (options.scale) params.scale = options.scale; // 1, 2, 4, etc.
942
+ }
943
+ return window.sendPluginCommand('CAPTURE_SCREENSHOT', params, 30000)
944
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
945
+ };
946
+
947
+ // Set image fill on nodes — decodes base64 in browser context (atob available here)
948
+ // then sends raw bytes to plugin where figma.createImage() is called
949
+ window.setImageFill = (nodeIds, imageData, scaleMode) => {
950
+ // Decode base64 to Uint8Array in browser context where atob() is available
951
+ var binaryStr = atob(imageData);
952
+ var bytes = new Uint8Array(binaryStr.length);
953
+ for (var i = 0; i < binaryStr.length; i++) {
954
+ bytes[i] = binaryStr.charCodeAt(i);
955
+ }
956
+ // Send as plain Array (postMessage can't always transfer typed arrays cleanly)
957
+ return window.sendPluginCommand('SET_IMAGE_FILL', {
958
+ nodeIds: Array.isArray(nodeIds) ? nodeIds : [nodeIds],
959
+ imageBytes: Array.from(bytes),
960
+ scaleMode: scaleMode || 'FILL'
961
+ }, 60000)
962
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
963
+ };
964
+
965
+ // Set component instance properties (TEXT, BOOLEAN, INSTANCE_SWAP, VARIANT)
966
+ // This is the correct way to update component instances vs direct text node editing
967
+ window.setInstanceProperties = (nodeId, properties) => {
968
+ return window.sendPluginCommand('SET_INSTANCE_PROPERTIES', {
969
+ nodeId: nodeId,
970
+ properties: properties
971
+ }).catch(function(err) { return { success: false, error: err.message || String(err) }; });
972
+ };
973
+
974
+ // Lint design for accessibility and quality issues
975
+ window.lintDesign = (nodeId, rules, maxDepth, maxFindings) => {
976
+ var params = {};
977
+ if (nodeId) params.nodeId = nodeId;
978
+ if (rules) params.rules = rules;
979
+ if (maxDepth !== undefined) params.maxDepth = maxDepth;
980
+ if (maxFindings !== undefined) params.maxFindings = maxFindings;
981
+ return window.sendPluginCommand('LINT_DESIGN', params, 120000)
982
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
983
+ };
984
+
985
+ // Audit component accessibility (deep a11y scorecard with color-blind simulation)
986
+ window.auditComponentAccessibility = function(nodeId, targetSize) {
987
+ var params = {};
988
+ if (nodeId) params.nodeId = nodeId;
989
+ if (targetSize !== undefined) params.targetSize = targetSize;
990
+ return window.sendPluginCommand('AUDIT_COMPONENT_ACCESSIBILITY', params, 120000)
991
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
992
+ };
993
+
994
+ // Analyze component set (variant state machine + cross-variant diff)
995
+ window.analyzeComponentSet = (nodeId) => {
996
+ return window.sendPluginCommand('ANALYZE_COMPONENT_SET', {
997
+ nodeId: nodeId
998
+ }, 30000)
999
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
1000
+ };
1001
+
1002
+ // Deep component extraction (full visual props, tokens, interactions at every level)
1003
+ window.deepGetComponent = (nodeId, depth) => {
1004
+ return window.sendPluginCommand('DEEP_GET_COMPONENT', {
1005
+ nodeId: nodeId,
1006
+ depth: depth || 10
1007
+ }, 30000)
1008
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
1009
+ };
1010
+
1011
+ // Get annotations from a node (and optionally its children)
1012
+ window.getAnnotations = (nodeId, includeChildren, depth) => {
1013
+ return window.sendPluginCommand('GET_ANNOTATIONS', {
1014
+ nodeId: nodeId,
1015
+ includeChildren: includeChildren,
1016
+ depth: depth
1017
+ }, 10000)
1018
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
1019
+ };
1020
+
1021
+ // Set annotations on a node
1022
+ window.setAnnotations = (nodeId, annotations, mode) => {
1023
+ return window.sendPluginCommand('SET_ANNOTATIONS', {
1024
+ nodeId: nodeId,
1025
+ annotations: annotations,
1026
+ mode: mode || 'replace'
1027
+ }, 10000)
1028
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
1029
+ };
1030
+
1031
+ // Get available annotation categories
1032
+ window.getAnnotationCategories = () => {
1033
+ return window.sendPluginCommand('GET_ANNOTATION_CATEGORIES', {}, 5000)
1034
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
1035
+ };
1036
+
1037
+ // ============================================================================
1038
+ // WEBSOCKET BRIDGE CLIENT — primary transport from the plugin sandbox to
1039
+ // the MCP server. Scans port range 9223–9232 for multiple instances.
1040
+ // ============================================================================
1041
+ (function() {
1042
+ // Port range for multi-instance support (matches server's port-discovery.ts)
1043
+ var WS_PORT_RANGE_START = 9223;
1044
+ var WS_PORT_RANGE_END = 9232;
1045
+
1046
+ // Multi-connection state: plugin connects to ALL active MCP servers
1047
+ // so that every Claude tab/CLI instance gets Figma access.
1048
+ var activeConnections = []; // Array of { port, ws, serverVersion?, serverInfo? }
1049
+ var isScanning = false;
1050
+
1051
+ // Backward-compat: ws and wsConnected reflect "at least one connection"
1052
+ var ws = null;
1053
+ var wsPort = null;
1054
+ var wsConnected = false;
1055
+
1056
+ // Method-to-function mapping
1057
+ var methodMap = {
1058
+ 'EXECUTE_CODE': function(params) { return window.executeCode(params.code, params.timeout); },
1059
+ 'UPDATE_VARIABLE': function(params) { return window.updateVariable(params.variableId, params.modeId, params.value); },
1060
+ 'CREATE_VARIABLE': function(params) { return window.createVariable(params.name, params.collectionId, params.resolvedType, params); },
1061
+ 'DELETE_VARIABLE': function(params) { return window.deleteVariable(params.variableId); },
1062
+ 'DELETE_VARIABLE_COLLECTION': function(params) { return window.deleteVariableCollection(params.collectionId); },
1063
+ 'RENAME_VARIABLE': function(params) { return window.renameVariable(params.variableId, params.newName); },
1064
+ 'SET_VARIABLE_DESCRIPTION': function(params) { return window.setVariableDescription(params.variableId, params.description); },
1065
+ 'ADD_MODE': function(params) { return window.addMode(params.collectionId, params.modeName); },
1066
+ 'RENAME_MODE': function(params) { return window.renameMode(params.collectionId, params.modeId, params.newName); },
1067
+ 'REFRESH_VARIABLES': function() { return window.refreshVariables(); },
1068
+ 'CREATE_VARIABLE_COLLECTION': function(params) { return window.createVariableCollection(params.name, params); },
1069
+ 'GET_LOCAL_COMPONENTS': function() { return window.getLocalComponents(); },
1070
+ 'INSTANTIATE_COMPONENT': function(params) { return window.instantiateComponent(params.componentKey, params); },
1071
+ // Component set creation — forward params wholesale; result payload rides in msg.data.
1072
+ // Timeout scales with variant count (the plugin builds all variants in one
1073
+ // uncancellable pass; a fixed 30s ceiling made big matrices report failure
1074
+ // while the set still got created). Mirrors componentSetTimeoutMs in
1075
+ // websocket-connector.ts, which adds a 5s buffer on the server hop so this
1076
+ // hop's result always arrives first — keep the base formula in sync.
1077
+ 'CREATE_COMPONENT_SET': function(params) {
1078
+ var variantCount = 1;
1079
+ if (params && params.componentIds && params.componentIds.length) {
1080
+ variantCount = params.componentIds.length;
1081
+ } else if (params && params.properties) {
1082
+ for (var axis in params.properties) {
1083
+ if (params.properties.hasOwnProperty(axis)) {
1084
+ var vals = params.properties[axis];
1085
+ variantCount *= Math.max(1, (vals && vals.length) || 1);
1086
+ }
1087
+ }
1088
+ }
1089
+ var timeout = Math.min(120000, Math.max(30000, variantCount * 1200));
1090
+ return window.sendPluginCommand('CREATE_COMPONENT_SET', params, timeout)
1091
+ .catch(function(err) { return { success: false, error: err.message || String(err) }; });
1092
+ },
1093
+ 'GET_COMPONENT': function(params) { return window.requestComponentData(params.nodeId); },
1094
+ 'SET_NODE_DESCRIPTION': function(params) { return window.setNodeDescription(params.nodeId, params.description, params.descriptionMarkdown); },
1095
+ 'ADD_COMPONENT_PROPERTY': function(params) { return window.addComponentProperty(params.nodeId, params.propertyName, params.propertyType, params.defaultValue, params); },
1096
+ 'EDIT_COMPONENT_PROPERTY': function(params) { return window.editComponentProperty(params.nodeId, params.propertyName, params.newValue); },
1097
+ 'DELETE_COMPONENT_PROPERTY': function(params) { return window.deleteComponentProperty(params.nodeId, params.propertyName); },
1098
+ 'CREATE_SLOT': function(params) { return window.createSlot(params.nodeId, params); },
1099
+ 'GET_SLOTS': function(params) { return window.getSlots(params.nodeId); },
1100
+ 'APPEND_TO_SLOT': function(params) { return window.appendToSlot(params); },
1101
+ 'RESET_SLOT': function(params) { return window.resetSlot(params); },
1102
+ 'RESIZE_NODE': function(params) { return window.resizeNode(params.nodeId, params.width, params.height, params.withConstraints); },
1103
+ 'MOVE_NODE': function(params) { return window.moveNode(params.nodeId, params.x, params.y); },
1104
+ 'SET_NODE_FILLS': function(params) { return window.setNodeFills(params.nodeId, params.fills); },
1105
+ 'SET_NODE_STROKES': function(params) { return window.setNodeStrokes(params.nodeId, params.strokes, params.strokeWeight); },
1106
+ 'SET_NODE_OPACITY': function(params) { return window.setNodeOpacity(params.nodeId, params.opacity); },
1107
+ 'SET_NODE_CORNER_RADIUS': function(params) { return window.setNodeCornerRadius(params.nodeId, params.radius); },
1108
+ 'CLONE_NODE': function(params) { return window.cloneNode(params.nodeId); },
1109
+ 'DELETE_NODE': function(params) { return window.deleteNode(params.nodeId); },
1110
+ 'RENAME_NODE': function(params) { return window.renameNode(params.nodeId, params.newName); },
1111
+ 'SET_TEXT_CONTENT': function(params) { return window.setTextContent(params.nodeId, params.text, params); },
1112
+ 'CREATE_CHILD_NODE': function(params) { return window.createChildNode(params.parentId, params.nodeType, params.properties); },
1113
+ 'CAPTURE_SCREENSHOT': function(params) { return window.captureScreenshot(params.nodeId, params); },
1114
+ 'SET_IMAGE_FILL': function(params) { return window.setImageFill(params.nodeIds || params.nodeId, params.imageData, params.scaleMode); },
1115
+ 'SET_INSTANCE_PROPERTIES': function(params) { return window.setInstanceProperties(params.nodeId, params.properties); },
1116
+ 'LINT_DESIGN': function(params) { return window.lintDesign(params.nodeId, params.rules, params.maxDepth, params.maxFindings); },
1117
+ 'AUDIT_COMPONENT_ACCESSIBILITY': function(params) { return window.auditComponentAccessibility(params.nodeId, params.targetSize); },
1118
+ 'GET_VARIABLES_DATA': function() {
1119
+ // Return the cached variables data directly
1120
+ if (window.__figmaVariablesReady && window.__figmaVariablesData) {
1121
+ return Promise.resolve(window.__figmaVariablesData);
1122
+ }
1123
+ return Promise.reject(new Error('Variables data not ready. Make sure the Desktop Bridge plugin has loaded.'));
1124
+ },
1125
+ 'GET_FILE_INFO': function() {
1126
+ return window.sendPluginCommand('GET_FILE_INFO', {});
1127
+ },
1128
+ // FigJam tools — forward directly to plugin code.js
1129
+ 'CREATE_STICKY': function(params) {
1130
+ return window.sendPluginCommand('CREATE_STICKY', params);
1131
+ },
1132
+ 'CREATE_STICKIES': function(params) {
1133
+ return window.sendPluginCommand('CREATE_STICKIES', params, 30000);
1134
+ },
1135
+ 'CREATE_CONNECTOR': function(params) {
1136
+ return window.sendPluginCommand('CREATE_CONNECTOR', params);
1137
+ },
1138
+ 'CREATE_SHAPE_WITH_TEXT': function(params) {
1139
+ return window.sendPluginCommand('CREATE_SHAPE_WITH_TEXT', params);
1140
+ },
1141
+ 'CREATE_SECTION': function(params) {
1142
+ return window.sendPluginCommand('CREATE_SECTION', params);
1143
+ },
1144
+ 'CREATE_TABLE': function(params) {
1145
+ return window.sendPluginCommand('CREATE_TABLE', params, 30000);
1146
+ },
1147
+ 'CREATE_CODE_BLOCK': function(params) {
1148
+ return window.sendPluginCommand('CREATE_CODE_BLOCK', params);
1149
+ },
1150
+ 'GET_BOARD_CONTENTS': function(params) {
1151
+ return window.sendPluginCommand('GET_BOARD_CONTENTS', params, 30000);
1152
+ },
1153
+ 'GET_CONNECTIONS': function(params) {
1154
+ return window.sendPluginCommand('GET_CONNECTIONS', params || {}, 15000);
1155
+ },
1156
+ // Slides tools — forward directly to plugin code.js
1157
+ 'LIST_SLIDES': function(params) {
1158
+ return window.sendPluginCommand('LIST_SLIDES', params || {}, 10000);
1159
+ },
1160
+ 'GET_SLIDE_CONTENT': function(params) {
1161
+ return window.sendPluginCommand('GET_SLIDE_CONTENT', params, 10000);
1162
+ },
1163
+ 'CREATE_SLIDE': function(params) {
1164
+ return window.sendPluginCommand('CREATE_SLIDE', params || {}, 10000);
1165
+ },
1166
+ 'DELETE_SLIDE': function(params) {
1167
+ return window.sendPluginCommand('DELETE_SLIDE', params, 5000);
1168
+ },
1169
+ 'DUPLICATE_SLIDE': function(params) {
1170
+ return window.sendPluginCommand('DUPLICATE_SLIDE', params, 5000);
1171
+ },
1172
+ 'GET_SLIDE_GRID': function(params) {
1173
+ return window.sendPluginCommand('GET_SLIDE_GRID', params || {}, 10000);
1174
+ },
1175
+ 'REORDER_SLIDES': function(params) {
1176
+ return window.sendPluginCommand('REORDER_SLIDES', params, 15000);
1177
+ },
1178
+ 'SET_SLIDE_TRANSITION': function(params) {
1179
+ return window.sendPluginCommand('SET_SLIDE_TRANSITION', params, 5000);
1180
+ },
1181
+ 'GET_SLIDE_TRANSITION': function(params) {
1182
+ return window.sendPluginCommand('GET_SLIDE_TRANSITION', params, 5000);
1183
+ },
1184
+ 'SET_SLIDES_VIEW_MODE': function(params) {
1185
+ return window.sendPluginCommand('SET_SLIDES_VIEW_MODE', params, 5000);
1186
+ },
1187
+ 'GET_FOCUSED_SLIDE': function(params) {
1188
+ return window.sendPluginCommand('GET_FOCUSED_SLIDE', params || {}, 5000);
1189
+ },
1190
+ 'FOCUS_SLIDE': function(params) {
1191
+ return window.sendPluginCommand('FOCUS_SLIDE', params, 5000);
1192
+ },
1193
+ 'SKIP_SLIDE': function(params) {
1194
+ return window.sendPluginCommand('SKIP_SLIDE', params, 5000);
1195
+ },
1196
+ 'GET_TEXT_STYLES': function(params) {
1197
+ return window.sendPluginCommand('GET_TEXT_STYLES', params, 5000);
1198
+ },
1199
+ 'SET_SLIDE_BACKGROUND': function(params) {
1200
+ return window.sendPluginCommand('SET_SLIDE_BACKGROUND', params, 5000);
1201
+ },
1202
+ 'ADD_TEXT_TO_SLIDE': function(params) {
1203
+ return window.sendPluginCommand('ADD_TEXT_TO_SLIDE', params, 10000);
1204
+ },
1205
+ 'ADD_SHAPE_TO_SLIDE': function(params) {
1206
+ return window.sendPluginCommand('ADD_SHAPE_TO_SLIDE', params, 5000);
1207
+ },
1208
+ 'CLEAR_CONSOLE': function() {
1209
+ // Console buffer is maintained server-side; this is a no-op ack
1210
+ return Promise.resolve({ cleared: true });
1211
+ },
1212
+ 'RELOAD_UI': function() {
1213
+ return window.sendPluginCommand('RELOAD_UI', {});
1214
+ },
1215
+ // Component set analysis — variant state machine + cross-variant diff
1216
+ 'ANALYZE_COMPONENT_SET': function(params) {
1217
+ return window.analyzeComponentSet(params.nodeId);
1218
+ },
1219
+ // Deep component extraction — full visual tree with tokens and interactions
1220
+ 'DEEP_GET_COMPONENT': function(params) {
1221
+ return window.deepGetComponent(params.nodeId, params.depth);
1222
+ },
1223
+ // Annotation tools — forward to plugin code.js
1224
+ 'GET_ANNOTATIONS': function(params) {
1225
+ return window.sendPluginCommand('GET_ANNOTATIONS', params, 10000);
1226
+ },
1227
+ 'SET_ANNOTATIONS': function(params) {
1228
+ return window.sendPluginCommand('SET_ANNOTATIONS', params, 10000);
1229
+ },
1230
+ 'GET_ANNOTATION_CATEGORIES': function(params) {
1231
+ return window.sendPluginCommand('GET_ANNOTATION_CATEGORIES', params || {}, 5000);
1232
+ }
1233
+ };
1234
+
1235
+ /**
1236
+ * Check if we already have a connection to a specific port.
1237
+ */
1238
+ function isPortConnected(port) {
1239
+ for (var i = 0; i < activeConnections.length; i++) {
1240
+ if (activeConnections[i].port === port && activeConnections[i].ws.readyState === 1) {
1241
+ return true;
1242
+ }
1243
+ }
1244
+ return false;
1245
+ }
1246
+
1247
+ /**
1248
+ * Remove a connection from the active list and update compat state.
1249
+ */
1250
+ function removeConnection(port) {
1251
+ activeConnections = activeConnections.filter(function(c) { return c.port !== port; });
1252
+ updateCompatState();
1253
+ // Re-pace the health discovery loop: dropping to zero connections
1254
+ // switches it to the fast (3s) cadence immediately.
1255
+ scheduleDiscovery();
1256
+ }
1257
+
1258
+ /**
1259
+ * Update backward-compat variables (ws, wsPort, wsConnected) and
1260
+ * re-render the status pill so the Local/Cloud mode suffix reflects
1261
+ * the new connection set.
1262
+ */
1263
+ function updateCompatState() {
1264
+ var live = activeConnections.filter(function(c) { return c.ws.readyState === 1; });
1265
+ wsConnected = live.length > 0;
1266
+ if (live.length > 0) {
1267
+ ws = live[0].ws;
1268
+ wsPort = live[0].port;
1269
+ } else {
1270
+ ws = null;
1271
+ wsPort = null;
1272
+ }
1273
+ // deriveAndRenderStatus is defined in the outer scope (same script tag,
1274
+ // hoisted). Guarded for the unlikely case it's invoked before then.
1275
+ if (typeof deriveAndRenderStatus === 'function') deriveAndRenderStatus();
1276
+ }
1277
+
1278
+ /**
1279
+ * Initialize a new connection to a server: send FILE_INFO, variables, attach handlers.
1280
+ */
1281
+ function initializeConnection(connWs, port) {
1282
+ // Forward cached variables if available
1283
+ if (window.__figmaVariablesReady && window.__figmaVariablesData) {
1284
+ connWs.send(JSON.stringify({
1285
+ type: 'VARIABLES_DATA',
1286
+ data: window.__figmaVariablesData
1287
+ }));
1288
+ }
1289
+
1290
+ // Proactively report file identity to this server
1291
+ window.sendPluginCommand('GET_FILE_INFO', {})
1292
+ .then(function(info) {
1293
+ if (connWs.readyState === 1 && info && info.success !== false) {
1294
+ connWs.send(JSON.stringify({ type: 'FILE_INFO', data: info.fileInfo || info }));
1295
+ }
1296
+ })
1297
+ .catch(function() { /* non-critical */ });
1298
+ }
1299
+
1300
+ /**
1301
+ * Scan the port range and connect to ALL active MCP servers.
1302
+ * Each server (e.g., Chat tab on 9223, Code tab on 9224) gets its own
1303
+ * independent WebSocket connection so every Claude instance has Figma access.
1304
+ * Falls back to retry with backoff if no servers found at all.
1305
+ */
1306
+ // Maximum scan attempts on initial load (prevents infinite error loops).
1307
+ // After connecting, disconnect-triggered retries have their own limit.
1308
+ var initialScanAttempts = 0;
1309
+ var MAX_INITIAL_SCANS = 3;
1310
+ // Set true only when the user clicks Pause. Suppresses the health
1311
+ // discovery loop so a deliberate pause is not undone automatically.
1312
+ var userPaused = false;
1313
+
1314
+ function wsScanAndConnect() {
1315
+ if (isScanning) return;
1316
+ isScanning = true;
1317
+
1318
+ var portsToTry = [];
1319
+ for (var p = WS_PORT_RANGE_START; p <= WS_PORT_RANGE_END; p++) {
1320
+ if (!isPortConnected(p)) portsToTry.push(p);
1321
+ }
1322
+
1323
+ if (portsToTry.length === 0) { isScanning = false; return; }
1324
+
1325
+ console.log('[MCP Bridge] Scanning ports ' + WS_PORT_RANGE_START + '-' + WS_PORT_RANGE_END + ' for MCP servers...');
1326
+
1327
+ var foundAny = false;
1328
+ var pending = portsToTry.length;
1329
+
1330
+ portsToTry.forEach(function(port) {
1331
+ try {
1332
+ var testWs = new WebSocket('ws://localhost:' + port);
1333
+
1334
+ var timeout = setTimeout(function() {
1335
+ if (testWs.readyState !== 1) testWs.close();
1336
+ }, 3000);
1337
+
1338
+ testWs.onopen = function() {
1339
+ clearTimeout(timeout);
1340
+ foundAny = true;
1341
+ activeConnections.push({ port: port, ws: testWs });
1342
+ updateCompatState();
1343
+ console.log('[MCP Bridge] WebSocket connected to port ' + port + ' (' + activeConnections.length + ' server(s) total)');
1344
+ attachWsHandlers(testWs, port);
1345
+ initializeConnection(testWs, port);
1346
+ pending--;
1347
+ if (pending <= 0) {
1348
+ isScanning = false;
1349
+ }
1350
+ };
1351
+
1352
+ testWs.onerror = function() {
1353
+ clearTimeout(timeout);
1354
+ };
1355
+
1356
+ testWs.onclose = function() {
1357
+ clearTimeout(timeout);
1358
+ pending--;
1359
+ if (pending <= 0) {
1360
+ isScanning = false;
1361
+ // Retry with backoff if no servers found, up to MAX_INITIAL_SCANS
1362
+ if (!foundAny && activeConnections.length === 0) {
1363
+ // Fast burst on load: a couple of quick retries with backoff.
1364
+ // Once the burst exhausts we STOP re-arming here — the HTTP
1365
+ // /health discovery loop takes over and keeps probing quietly
1366
+ // (fetch to a closed port rejects catchably, no console spam),
1367
+ // so a server that starts AFTER the plugin still connects
1368
+ // without a restart.
1369
+ if (initialScanAttempts < MAX_INITIAL_SCANS) {
1370
+ initialScanAttempts++;
1371
+ if (initialScanAttempts < MAX_INITIAL_SCANS) {
1372
+ var delay = 3000 * initialScanAttempts; // 3s, 6s
1373
+ console.log('[MCP Bridge] No servers found, retry ' + initialScanAttempts + '/' + MAX_INITIAL_SCANS + ' in ' + (delay/1000) + 's');
1374
+ setTimeout(wsScanAndConnect, delay);
1375
+ } else {
1376
+ console.log('[MCP Bridge] No MCP server yet — health discovery keeps probing every ' + (DISCOVERY_IDLE_MS/1000) + 's until one appears (no restart needed).');
1377
+ }
1378
+ }
1379
+ }
1380
+ }
1381
+ };
1382
+ } catch (e) {
1383
+ pending--;
1384
+ }
1385
+ });
1386
+ }
1387
+
1388
+ /**
1389
+ * Reconnect a specific port that disconnected.
1390
+ * Tries the same port once (server may have just restarted); anything
1391
+ * slower is picked up by the /health discovery loop.
1392
+ */
1393
+ function wsReconnectPort(port) {
1394
+ if (port === 'cloud') {
1395
+ // The cloud connection is a wss:// relay, not a localhost port —
1396
+ // 'ws://localhost:cloud' is an invalid URL. Re-dial through the
1397
+ // cloud code (correct URL + its own bounded retry/give-up).
1398
+ if (typeof window.__cloudReconnect === 'function') window.__cloudReconnect();
1399
+ return;
1400
+ }
1401
+ if (userPaused || isPortConnected(port)) return;
1402
+ try {
1403
+ var testWs = new WebSocket('ws://localhost:' + port);
1404
+ var timeout = setTimeout(function() {
1405
+ if (testWs.readyState !== 1) testWs.close();
1406
+ }, 2000);
1407
+
1408
+ testWs.onopen = function() {
1409
+ clearTimeout(timeout);
1410
+ activeConnections.push({ port: port, ws: testWs });
1411
+ updateCompatState();
1412
+ console.log('[MCP Bridge] Reconnected to port ' + port + ' (' + activeConnections.length + ' server(s) total)');
1413
+ attachWsHandlers(testWs, port);
1414
+ initializeConnection(testWs, port);
1415
+ };
1416
+
1417
+ testWs.onerror = function() {
1418
+ clearTimeout(timeout);
1419
+ };
1420
+ } catch (e) {
1421
+ // Port gone — the discovery loop will find it if it comes back
1422
+ }
1423
+ }
1424
+
1425
+ /**
1426
+ * Connect directly to a port the /health probe just verified is OUR
1427
+ * server. Same attach path as the scan, plus the server version the
1428
+ * probe reported (stored on the connection for display/diagnostics).
1429
+ */
1430
+ function wsConnectToPort(port, serverVersion) {
1431
+ if (userPaused || isPortConnected(port)) return;
1432
+ try {
1433
+ var testWs = new WebSocket('ws://localhost:' + port);
1434
+ var timeout = setTimeout(function() {
1435
+ if (testWs.readyState !== 1) testWs.close();
1436
+ }, 3000);
1437
+
1438
+ testWs.onopen = function() {
1439
+ clearTimeout(timeout);
1440
+ var conn = { port: port, ws: testWs };
1441
+ if (serverVersion) conn.serverVersion = serverVersion;
1442
+ activeConnections.push(conn);
1443
+ updateCompatState();
1444
+ console.log('[MCP Bridge] WebSocket connected to port ' + port + (serverVersion ? ' (server v' + serverVersion + ')' : '') + ' (' + activeConnections.length + ' server(s) total)');
1445
+ attachWsHandlers(testWs, port);
1446
+ initializeConnection(testWs, port);
1447
+ };
1448
+
1449
+ testWs.onerror = function() {
1450
+ clearTimeout(timeout);
1451
+ };
1452
+ } catch (e) {
1453
+ // ignore — next discovery pass will retry
1454
+ }
1455
+ }
1456
+
1457
+ /**
1458
+ * Attach message/close/error handlers to an established WebSocket connection.
1459
+ */
1460
+ function attachWsHandlers(activeWs, port) {
1461
+ activeWs.onmessage = function(event) {
1462
+ try {
1463
+ var message = JSON.parse(event.data);
1464
+
1465
+ // Handle server identity messages
1466
+ if (message.type === 'SERVER_HELLO' && message.data) {
1467
+ console.log('[MCP Bridge] Connected to server on port ' + message.data.port + ' (PID: ' + message.data.pid + ', v' + message.data.serverVersion + ')');
1468
+ var conn = activeConnections.find(function(c) { return c.ws === activeWs; });
1469
+ if (conn) {
1470
+ conn.serverInfo = message.data;
1471
+ if (message.data.serverVersion) conn.serverVersion = message.data.serverVersion;
1472
+ }
1473
+ return;
1474
+ }
1475
+
1476
+ // Server says the installed plugin is older than it expects.
1477
+ if (message.type === 'PLUGIN_UPDATE_AVAILABLE') {
1478
+ if (typeof showUpdateBanner === 'function') showUpdateBanner();
1479
+ return;
1480
+ }
1481
+
1482
+ if (!message.id || !message.method) {
1483
+ console.log('[MCP Bridge] WS:' + port + ': Ignoring malformed message');
1484
+ return;
1485
+ }
1486
+
1487
+ var handler = methodMap[message.method];
1488
+ if (!handler) {
1489
+ activeWs.send(JSON.stringify({ id: message.id, error: 'Unknown method: ' + message.method }));
1490
+ return;
1491
+ }
1492
+
1493
+ // Call the handler with a safety timeout, then send back the result
1494
+ var HANDLER_TIMEOUT_MS = 30000;
1495
+ var handlerSettled = false;
1496
+ var handlerTimeoutId = null;
1497
+ var handlerPromise = Promise.resolve(handler(message.params || {}));
1498
+ var timeoutPromise = new Promise(function(_, reject) {
1499
+ handlerTimeoutId = setTimeout(function() {
1500
+ if (!handlerSettled) {
1501
+ reject(new Error('Handler for ' + message.method + ' did not respond within ' + HANDLER_TIMEOUT_MS + 'ms'));
1502
+ }
1503
+ }, HANDLER_TIMEOUT_MS);
1504
+ });
1505
+
1506
+ Promise.race([handlerPromise, timeoutPromise])
1507
+ .then(function(result) {
1508
+ handlerSettled = true;
1509
+ if (handlerTimeoutId !== null) clearTimeout(handlerTimeoutId);
1510
+ if (activeWs.readyState === 1) {
1511
+ activeWs.send(JSON.stringify({ id: message.id, result: result }));
1512
+ } else {
1513
+ console.warn('[MCP Bridge] WS:' + port + ': Dropping result for ' + message.method + ' (id=' + message.id + ') — socket closed');
1514
+ }
1515
+ })
1516
+ .catch(function(err) {
1517
+ handlerSettled = true;
1518
+ if (handlerTimeoutId !== null) clearTimeout(handlerTimeoutId);
1519
+ if (activeWs.readyState === 1) {
1520
+ activeWs.send(JSON.stringify({ id: message.id, error: err.message || String(err) }));
1521
+ } else {
1522
+ console.warn('[MCP Bridge] WS:' + port + ': Dropping error for ' + message.method + ' (id=' + message.id + ') — socket closed: ' + (err.message || String(err)));
1523
+ }
1524
+ });
1525
+ } catch (e) {
1526
+ console.error('[MCP Bridge] WS:' + port + ': Failed to process message:', e);
1527
+ }
1528
+ };
1529
+
1530
+ activeWs.onclose = function(event) {
1531
+ removeConnection(port);
1532
+ console.log('[MCP Bridge] WebSocket disconnected from port ' + port + ' (' + activeConnections.length + ' server(s) remaining)');
1533
+
1534
+ // If replaced by same file reconnecting (e.g., plugin reloaded), stop
1535
+ var wasReplaced = (event.code === 1000 && (
1536
+ event.reason === 'Replaced by new connection' ||
1537
+ event.reason === 'Replaced by same file reconnection'
1538
+ ));
1539
+ // If user paused via the Pause button, also stop auto-reconnect.
1540
+ var wasManualPause = (event.code === 1000 && event.reason === 'Manual disconnect');
1541
+ if (wasReplaced || wasManualPause) {
1542
+ console.log('[MCP Bridge] WebSocket:' + port + ': stopped by ' + (wasManualPause ? 'user pause' : 'replacement'));
1543
+ return;
1544
+ }
1545
+
1546
+ // One immediate same-port retry covers fast same-port server
1547
+ // restarts; anything slower is picked up by the /health discovery
1548
+ // loop, so the old capped retry-with-backoff dance is gone.
1549
+ setTimeout(function() { wsReconnectPort(port); }, 1000);
1550
+ };
1551
+
1552
+ activeWs.onerror = function() {
1553
+ // onclose will fire after this, triggering reconnect
1554
+ };
1555
+ }
1556
+
1557
+ /**
1558
+ * Broadcast a message to ALL active WebSocket connections.
1559
+ * Events like variable changes, selections, document changes need to
1560
+ * reach every MCP server instance so they all have current state.
1561
+ */
1562
+ function broadcastToAll(message) {
1563
+ var json = JSON.stringify(message);
1564
+ activeConnections.forEach(function(conn) {
1565
+ if (conn.ws.readyState === 1) {
1566
+ try { conn.ws.send(json); } catch(e) { /* ignore send errors */ }
1567
+ }
1568
+ });
1569
+ }
1570
+
1571
+ // Forward VARIABLES_DATA to all connected MCP servers
1572
+ window.__wsForwardVariables = function(data) {
1573
+ if (wsConnected) {
1574
+ broadcastToAll({ type: 'VARIABLES_DATA', data: data });
1575
+ }
1576
+ };
1577
+
1578
+ // Forward DOCUMENT_CHANGE events to all servers for cache invalidation
1579
+ window.__wsForwardDocumentChange = function(data) {
1580
+ if (wsConnected) {
1581
+ broadcastToAll({ type: 'DOCUMENT_CHANGE', data: data });
1582
+ }
1583
+ };
1584
+
1585
+ // v1.25.0: forward METADATA_CHANGE events (description/annotation edits)
1586
+ // so figma_diff_versions can surface them despite Figma REST not exposing them.
1587
+ window.__wsForwardMetadataChange = function(data) {
1588
+ if (wsConnected) {
1589
+ broadcastToAll({ type: 'METADATA_CHANGE', data: data });
1590
+ }
1591
+ };
1592
+
1593
+ // Forward CONSOLE_CAPTURE events to all servers for console monitoring
1594
+ window.__wsForwardConsoleCapture = function(data) {
1595
+ if (wsConnected) {
1596
+ broadcastToAll({ type: 'CONSOLE_CAPTURE', data: data });
1597
+ }
1598
+ };
1599
+
1600
+ // Forward SELECTION_CHANGE events to all servers for selection tracking
1601
+ window.__wsForwardSelectionChange = function(data) {
1602
+ if (wsConnected) {
1603
+ broadcastToAll({ type: 'SELECTION_CHANGE', data: data });
1604
+ }
1605
+ };
1606
+
1607
+ // Forward PAGE_CHANGE events to all servers for page tracking
1608
+ window.__wsForwardPageChange = function(data) {
1609
+ if (wsConnected) {
1610
+ broadcastToAll({ type: 'PAGE_CHANGE', data: data });
1611
+ }
1612
+ };
1613
+
1614
+ // Expose functions for Cloud Mode to add connections to the same pool
1615
+ window.__wsAddCloudConnection = function(cloudWs, label, onDisconnect) {
1616
+ activeConnections.push({ port: label, ws: cloudWs });
1617
+ updateCompatState();
1618
+ attachWsHandlers(cloudWs, label);
1619
+ // Chain cloud disconnect callback after attachWsHandlers' onclose
1620
+ if (onDisconnect) {
1621
+ var origOnClose = cloudWs.onclose;
1622
+ cloudWs.onclose = function(event) {
1623
+ if (origOnClose) origOnClose.call(cloudWs, event);
1624
+ onDisconnect();
1625
+ };
1626
+ }
1627
+ initializeConnection(cloudWs, label);
1628
+ };
1629
+
1630
+ window.__wsGetActiveConnections = function() { return activeConnections; };
1631
+
1632
+ window.__wsDisconnectAll = function() {
1633
+ for (var i = 0; i < activeConnections.length; i++) {
1634
+ try { activeConnections[i].ws.close(1000, 'Manual disconnect'); } catch (e) {}
1635
+ }
1636
+ activeConnections = [];
1637
+ ws = null;
1638
+ wsConnected = false;
1639
+ // Reset scan state so a future Resume gets a clean retry budget.
1640
+ isScanning = false;
1641
+ initialScanAttempts = 0;
1642
+ // Deliberate user pause — keep discovery quiet until Resume.
1643
+ userPaused = true;
1644
+ };
1645
+
1646
+ window.__wsScanAndConnect = wsScanAndConnect;
1647
+
1648
+ // Manual (re)connect from the UI: clear any pause, refresh the retry budget
1649
+ // so the user gets the responsive fast burst again, then scan.
1650
+ window.__wsManualScan = function() {
1651
+ userPaused = false;
1652
+ initialScanAttempts = 0;
1653
+ wsScanAndConnect();
1654
+ };
1655
+
1656
+ window.__wsIsPaused = function() { return userPaused; };
1657
+ window.__wsIsScanning = function() { return isScanning; };
1658
+ // The health discovery loop is armed whenever the user hasn't paused —
1659
+ // used by the status pill to show "Looking for your AI app…" honestly.
1660
+ window.__wsIsDiscoveryActive = function() { return !userPaused; };
1661
+
1662
+ // ======================================================================
1663
+ // HTTP /health DISCOVERY LOOP — replaces the old zero-connection-gated
1664
+ // WS watchdog. fetch() to a closed port rejects catchably with NO
1665
+ // console spam (unlike new WebSocket()), so this can run always-on:
1666
+ // - every 3s while zero connections (fast attach after server start)
1667
+ // - every 10s while connected (catches a restarted instance among
1668
+ // live ones — previously a permanent dead end)
1669
+ // A port only gets a WS dial after its /health response matches OUR
1670
+ // server's payload shape ({status:'ok', version, clients, ...}).
1671
+ // ======================================================================
1672
+ var HEALTH_TIMEOUT_MS = 1500;
1673
+ var DISCOVERY_IDLE_MS = 3000; // cadence with zero connections
1674
+ var DISCOVERY_STEADY_MS = 10000; // cadence with >=1 connection
1675
+ var discoveryTimer = null;
1676
+ var discoveryInFlight = false;
1677
+
1678
+ function liveConnectionCount() {
1679
+ var n = 0;
1680
+ for (var i = 0; i < activeConnections.length; i++) {
1681
+ if (activeConnections[i].ws.readyState === 1) n++;
1682
+ }
1683
+ return n;
1684
+ }
1685
+
1686
+ function isOurHealthPayload(json) {
1687
+ // Shape served by websocket-server.ts handleHttpRequest()
1688
+ return !!(json && json.status === 'ok' && typeof json.version === 'string' && json.clients !== undefined);
1689
+ }
1690
+
1691
+ function probePortHealth(port) {
1692
+ var opts = {};
1693
+ try {
1694
+ if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
1695
+ opts.signal = AbortSignal.timeout(HEALTH_TIMEOUT_MS);
1696
+ }
1697
+ } catch (e) { /* AbortSignal unavailable — fetch just runs untimed */ }
1698
+ return fetch('http://localhost:' + port + '/health', opts)
1699
+ .then(function(res) {
1700
+ if (!res.ok) return null;
1701
+ return res.json().then(function(json) {
1702
+ return isOurHealthPayload(json) ? { port: port, version: json.version } : null;
1703
+ });
1704
+ })
1705
+ .catch(function() { return null; }); // closed port / timeout / non-JSON — all quiet
1706
+ }
1707
+
1708
+ function scheduleDiscovery() {
1709
+ if (discoveryTimer) clearTimeout(discoveryTimer);
1710
+ var delay = liveConnectionCount() > 0 ? DISCOVERY_STEADY_MS : DISCOVERY_IDLE_MS;
1711
+ discoveryTimer = setTimeout(runDiscoveryPass, delay);
1712
+ }
1713
+
1714
+ function runDiscoveryPass() {
1715
+ if (discoveryInFlight) { scheduleDiscovery(); return; }
1716
+ if (userPaused) { scheduleDiscovery(); return; } // never auto-connect while paused
1717
+ var candidates = [];
1718
+ for (var p = WS_PORT_RANGE_START; p <= WS_PORT_RANGE_END; p++) {
1719
+ if (!isPortConnected(p)) candidates.push(p);
1720
+ }
1721
+ if (candidates.length === 0) { scheduleDiscovery(); return; }
1722
+ discoveryInFlight = true;
1723
+ Promise.all(candidates.map(probePortHealth))
1724
+ .then(function(results) {
1725
+ for (var i = 0; i < results.length; i++) {
1726
+ var hit = results[i];
1727
+ if (hit && !userPaused && !isPortConnected(hit.port)) {
1728
+ wsConnectToPort(hit.port, hit.version);
1729
+ }
1730
+ }
1731
+ })
1732
+ .catch(function() { /* individual probes already swallow errors */ })
1733
+ .then(function() {
1734
+ discoveryInFlight = false;
1735
+ scheduleDiscovery();
1736
+ });
1737
+ }
1738
+
1739
+ // Initial scan on load (fast WS burst for instant attach). The discovery
1740
+ // loop then keeps probing forever: fast while disconnected, slow while
1741
+ // connected. Disconnect-triggered same-port retries handle quick drops.
1742
+ wsScanAndConnect();
1743
+ scheduleDiscovery();
1744
+ })();
1745
+
1746
+ // ============================================================================
1747
+ // CLOUD MODE — Connect to remote relay via pairing code
1748
+ // ============================================================================
1749
+ var cloudWs = null;
1750
+ var CLOUD_RELAY_HOST = 'wss://figma-console-mcp.southleft.com';
1751
+ var _lastCloudCode = null; // most recent code that successfully paired
1752
+ var _cloudUserDisconnected = false; // user clicked Disconnect — suppress auto re-dial
1753
+ var _cloudWasConnectedBeforePause = false; // restore cloud on Resume after Pause
1754
+ var _cloudRetryCount = 0; // bounded auto-reconnect budget
1755
+ var CLOUD_MAX_RETRIES = 5;
1756
+ var _cloudEverConnected = false; // paired successfully THIS session — separates a real
1757
+ // drop (worth telling the user) from a stale restored
1758
+ // code that never connected (stay quiet)
1759
+
1760
+ // ============================================================================
1761
+ // 3-STAGE UI TOGGLES
1762
+ // Stage 1: row-top (always visible)
1763
+ // Stage 2: sub-toolbar (revealed by [+])
1764
+ // Stage 3: log panel (revealed by Show log)
1765
+ // Cloud pairing is an independent row triggered by the cloud icon.
1766
+ // ============================================================================
1767
+
1768
+ // Fixed 240px width matches Figma right-side nav min.
1769
+ // Height grows freely with content. If the plugin extends past Figma's
1770
+ // visible UI, the user drags the plugin window to reveal what's clipped.
1771
+ var PLUGIN_WIDTH = 240;
1772
+
1773
+ function sendResize() {
1774
+ // Force a layout pass so measurements reflect the current visible rows.
1775
+ void document.body.offsetHeight;
1776
+ // Measure the inner content element + body padding. Avoids stale
1777
+ // scrollHeight values when the iframe was previously larger.
1778
+ var wrap = document.querySelector('.wrap');
1779
+ if (!wrap) return;
1780
+ var bs = window.getComputedStyle(document.body);
1781
+ var h = wrap.offsetHeight
1782
+ + (parseFloat(bs.paddingTop) || 0)
1783
+ + (parseFloat(bs.paddingBottom) || 0);
1784
+ parent.postMessage({
1785
+ pluginMessage: { type: 'RESIZE_UI', width: PLUGIN_WIDTH, height: Math.ceil(h) }
1786
+ }, '*');
1787
+ }
1788
+
1789
+ function autoResize() {
1790
+ sendResize(); // immediate — fires before next paint so Figma can grow upward in the same frame
1791
+ requestAnimationFrame(function() {
1792
+ sendResize();
1793
+ setTimeout(sendResize, 150);
1794
+ });
1795
+ }
1796
+
1797
+ // Observe the wrap (actual content), not body, so size changes from any
1798
+ // row toggle fire a resize whether the rows grew or shrank.
1799
+ if (typeof ResizeObserver !== 'undefined') {
1800
+ var _wrapObserve = function() {
1801
+ var wrap = document.querySelector('.wrap');
1802
+ if (wrap) { var ro = new ResizeObserver(sendResize); ro.observe(wrap); }
1803
+ };
1804
+ _wrapObserve();
1805
+ }
1806
+
1807
+ // Initial tighten — multiple attempts at different times to defeat any
1808
+ // residual Figma-side iframe sizing from a previous plugin state.
1809
+ sendResize();
1810
+ requestAnimationFrame(sendResize);
1811
+ window.addEventListener('load', function() {
1812
+ sendResize();
1813
+ setTimeout(sendResize, 50);
1814
+ setTimeout(sendResize, 200);
1815
+ setTimeout(sendResize, 600);
1816
+ });
1817
+
1818
+ function toggleRow(id, buttonId) {
1819
+ var row = document.getElementById(id);
1820
+ var btn = buttonId ? document.getElementById(buttonId) : null;
1821
+ var opening = !row.classList.contains('visible');
1822
+ row.classList.toggle('visible', opening);
1823
+ if (btn) {
1824
+ btn.classList.toggle('active', opening);
1825
+ // Keep aria-expanded in sync for screen readers.
1826
+ if (btn.hasAttribute('aria-expanded')) {
1827
+ btn.setAttribute('aria-expanded', opening ? 'true' : 'false');
1828
+ }
1829
+ }
1830
+ autoResize();
1831
+ return opening;
1832
+ }
1833
+
1834
+ function closeSubToolbarIfOpen() {
1835
+ var sub = document.getElementById('sub-toolbar');
1836
+ if (sub && sub.classList.contains('visible')) toggleSubToolbar();
1837
+ }
1838
+
1839
+ function closeCloudPairIfOpen() {
1840
+ var cp = document.getElementById('cloud-pair');
1841
+ if (cp && cp.classList.contains('visible')) {
1842
+ toggleRow('cloud-pair', 'cloud-icon');
1843
+ // Close the help too if it was open
1844
+ var ch = document.getElementById('cloud-help');
1845
+ if (ch && ch.classList.contains('visible')) toggleRow('cloud-help', 'cloud-info-btn');
1846
+ }
1847
+ }
1848
+
1849
+ function toggleCloudPair() {
1850
+ var opening = !document.getElementById('cloud-pair').classList.contains('visible');
1851
+ if (opening) closeSubToolbarIfOpen();
1852
+ toggleRow('cloud-pair', 'cloud-icon');
1853
+ // Re-derive on both open and close: opening shows the current cloud
1854
+ // state (previously it could open onto a stale blank), closing hides
1855
+ // routine states while keeping actionable ones ("pairing lost") visible.
1856
+ renderCloudStatus();
1857
+ }
1858
+
1859
+ function toggleCloudHelp() {
1860
+ toggleRow('cloud-help', 'cloud-info-btn');
1861
+ }
1862
+
1863
+ function toggleSubToolbar() {
1864
+ var opening = !document.getElementById('sub-toolbar').classList.contains('visible');
1865
+ if (opening) closeCloudPairIfOpen();
1866
+ var nowOpen = toggleRow('sub-toolbar', 'expand-btn');
1867
+ document.getElementById('expand-btn').textContent = nowOpen ? '−' : '+';
1868
+ if (!nowOpen) {
1869
+ // Collapse the Activity panel if sub-toolbar closes
1870
+ var log = document.getElementById('log-panel');
1871
+ if (log.classList.contains('visible')) toggleLog();
1872
+ }
1873
+ }
1874
+
1875
+ function toggleLog() {
1876
+ var logVisible = document.getElementById('log-panel').classList.contains('visible');
1877
+ var opening = !logVisible;
1878
+ toggleRow('log-panel', 'log-toggle');
1879
+ // Reveal the Copy button only when the Activity panel is open
1880
+ document.getElementById('copy-log-btn').style.display = opening ? '' : 'none';
1881
+ autoResize();
1882
+ }
1883
+
1884
+ // Follow Figma's theme. With themeColors:true, Figma adds a "figma-light" /
1885
+ // "figma-dark" class to <html> and updates it live when the user switches
1886
+ // themes. We mirror that onto body[data-theme] so the status/log color
1887
+ // tokens track Figma. Falls back to OS preference if the class is absent.
1888
+ function applyTheme() {
1889
+ var root = document.documentElement;
1890
+ var theme;
1891
+ if (root.classList.contains('figma-light')) theme = 'light';
1892
+ else if (root.classList.contains('figma-dark')) theme = 'dark';
1893
+ else theme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) ? 'light' : 'dark';
1894
+ document.body.setAttribute('data-theme', theme);
1895
+ }
1896
+
1897
+ applyTheme();
1898
+ // React to live Figma theme switches (class changes on <html>).
1899
+ if (window.MutationObserver) {
1900
+ new MutationObserver(applyTheme).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
1901
+ }
1902
+ // Fallback path: only relevant when no Figma theme class is present.
1903
+ if (window.matchMedia) {
1904
+ window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', applyTheme);
1905
+ }
1906
+
1907
+
1908
+ // ============================================================================
1909
+ // STAGE 2 — LOG PANEL, INFO PANEL, CONNECTION, CLIPBOARD EXPORT
1910
+ // ============================================================================
1911
+
1912
+ var PLUGIN_VERSION = 'v0.3.0';
1913
+ var logHistory = [];
1914
+ var logEntriesEl = null;
1915
+ var _ctaBtn = null;
1916
+ // Timestamp of the most recent command sent through the bridge (the same
1917
+ // event that appends to logHistory) — drives "Last action: Xs ago".
1918
+ var _lastActionAt = null;
1919
+
1920
+ function ensureLogRefs() {
1921
+ if (!logEntriesEl) logEntriesEl = document.getElementById('log-entries');
1922
+ if (!_ctaBtn) _ctaBtn = document.getElementById('cta-btn');
1923
+ }
1924
+
1925
+ function escHtml(s) {
1926
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1927
+ }
1928
+
1929
+ function log(message, level, ts) {
1930
+ level = level || 'info';
1931
+ ensureLogRefs();
1932
+ if (!logEntriesEl) return;
1933
+
1934
+ // Deduplicate: consecutive identical message+level bumps the count badge.
1935
+ var last = logEntriesEl.lastElementChild;
1936
+ if (last && last.dataset.logMsg === message && last.dataset.logLevel === level) {
1937
+ var n = (parseInt(last.dataset.logCount, 10) || 1) + 1;
1938
+ last.dataset.logCount = n;
1939
+ var tsEl = last.querySelector('.log-ts');
1940
+ var countEl = last.querySelector('.log-count');
1941
+ if (tsEl && ts) tsEl.textContent = ts;
1942
+ if (countEl) countEl.textContent = '×' + n;
1943
+ return;
1944
+ }
1945
+
1946
+ var entry = document.createElement('div');
1947
+ entry.className = 'log-entry ' + level;
1948
+ entry.dataset.logMsg = message;
1949
+ entry.dataset.logLevel = level;
1950
+ entry.dataset.logCount = '1';
1951
+ entry.innerHTML =
1952
+ '<span class="log-ts">' + escHtml(ts || '') + '</span>' +
1953
+ '<span class="log-msg" title="' + escHtml(message) + '">' + escHtml(message) + '</span>' +
1954
+ '<span class="log-dur"></span>' +
1955
+ '<span class="log-count"></span>';
1956
+ logEntriesEl.appendChild(entry);
1957
+ logEntriesEl.scrollTop = logEntriesEl.scrollHeight;
1958
+ while (logEntriesEl.children.length > 50) {
1959
+ logEntriesEl.removeChild(logEntriesEl.children[0]);
1960
+ }
1961
+ }
1962
+
1963
+ function logWithHistory(message, level) {
1964
+ level = level || 'info';
1965
+ var now = new Date();
1966
+ var ts = ('0' + now.getHours()).slice(-2) + ':' +
1967
+ ('0' + now.getMinutes()).slice(-2) + ':' +
1968
+ ('0' + now.getSeconds()).slice(-2);
1969
+ logHistory.push({ ts: now.toISOString().replace('T', ' ').replace(/\.\d+Z/, ''), level: level, message: message });
1970
+ log(message, level, ts);
1971
+ }
1972
+
1973
+ function copyLogToClipboard() {
1974
+ var lines = logHistory.map(function(e) {
1975
+ var prefix = e.level === 'error' ? '[!] ' : e.level === 'warn' ? '[WARN] ' : '';
1976
+ return e.ts + ' ' + prefix + e.message;
1977
+ });
1978
+ var text = 'Figma Desktop Bridge - Session Log\n'
1979
+ + 'Exported: ' + new Date().toISOString() + '\n'
1980
+ + 'Plugin: ' + PLUGIN_VERSION + '\n'
1981
+ + '------------------------------------------------\n'
1982
+ + lines.join('\n') + '\n';
1983
+ var ta = document.createElement('textarea');
1984
+ ta.value = text;
1985
+ ta.style.position = 'fixed';
1986
+ ta.style.opacity = '0';
1987
+ document.body.appendChild(ta);
1988
+ ta.select();
1989
+ var ok = false;
1990
+ try { ok = document.execCommand('copy'); } catch (e) {}
1991
+ document.body.removeChild(ta);
1992
+ if (ok) {
1993
+ logWithHistory('Copied to clipboard (' + logHistory.length + ' entries)', 'success');
1994
+ } else {
1995
+ logWithHistory('Copy failed - clipboard not available', 'error');
1996
+ }
1997
+ }
1998
+
1999
+ // Smart command summariser: turns raw message types and EXECUTE_CODE
2000
+ // payloads into human-readable log lines.
2001
+ var COMMAND_LABELS = {
2002
+ 'GET_FILE_INFO': 'Get file info',
2003
+ 'REFRESH_VARIABLES': 'Refresh variables',
2004
+ 'GET_LOCAL_COMPONENTS': 'Get local components',
2005
+ 'CLEAR_CONSOLE': 'Clear console',
2006
+ 'RELOAD_UI': 'Reload UI',
2007
+ 'GET_VARIABLES_DATA': 'Get variables data',
2008
+ 'RESIZE_UI': null // internal, don't log
2009
+ };
2010
+
2011
+ var FIGMA_API_PATTERNS = [
2012
+ { re: /figma\.create(\w+)/, fn: function(m) { return 'Create ' + camelToWords(m[1]); } },
2013
+ { re: /figma\.getNodeByIdAsync/, fn: function() { return 'Get node'; } },
2014
+ { re: /figma\.setCurrentPageAsync/, fn: function() { return 'Switch page'; } },
2015
+ { re: /figma\.loadFontAsync/, fn: function() { return 'Load font'; } },
2016
+ { re: /figma\.loadAllPagesAsync/, fn: function() { return 'Load all pages'; } },
2017
+ { re: /figma\.currentPage\.findAll/, fn: function() { return 'Find nodes'; } },
2018
+ { re: /figma\.currentPage\.findOne/, fn: function() { return 'Find node'; } },
2019
+ { re: /figma\.(union|subtract|intersect|flatten)\b/, fn: function(m) { return 'Boolean ' + m[1]; } },
2020
+ { re: /\.exportAsync/, fn: function() { return 'Export'; } },
2021
+ { re: /\.clone\(\)/, fn: function() { return 'Clone node'; } },
2022
+ { re: /\.remove\(\)/, fn: function() { return 'Remove node'; } },
2023
+ { re: /\.appendChild\b/, fn: function() { return 'Append child'; } },
2024
+ { re: /\.insertChild\b/, fn: function() { return 'Insert child'; } },
2025
+ { re: /\.resize\(/, fn: function() { return 'Resize'; } },
2026
+ { re: /\.characters\s*=/, fn: function() { return 'Set text'; } },
2027
+ { re: /\.fills\s*=/, fn: function() { return 'Set fills'; } },
2028
+ { re: /\.strokes\s*=/, fn: function() { return 'Set strokes'; } },
2029
+ { re: /\.effects\s*=/, fn: function() { return 'Set effects'; } },
2030
+ { re: /combineAsVariants/, fn: function() { return 'Combine as variants'; } },
2031
+ { re: /swapComponent/, fn: function() { return 'Swap component'; } }
2032
+ ];
2033
+
2034
+ function camelToWords(s) {
2035
+ return s.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
2036
+ }
2037
+
2038
+ function truncate(s, max) {
2039
+ max = max || 60;
2040
+ if (s.length <= max) return s;
2041
+ return s.substring(0, max - 1) + '…';
2042
+ }
2043
+
2044
+ function summariseCommand(type, params) {
2045
+ if (type === 'EXECUTE_CODE' && params && params.code) {
2046
+ var firstLine = params.code.trim().split('\n')[0].trim();
2047
+ if (firstLine.indexOf('//') === 0) {
2048
+ return truncate(firstLine.replace(/^\/\/\s*/, '').replace(/[—–]/g, '-'));
2049
+ }
2050
+ var hits = [];
2051
+ for (var i = 0; i < FIGMA_API_PATTERNS.length; i++) {
2052
+ var m = params.code.match(FIGMA_API_PATTERNS[i].re);
2053
+ if (m) hits.push(FIGMA_API_PATTERNS[i].fn(m));
2054
+ }
2055
+ if (hits.length > 0) {
2056
+ var unique = hits.filter(function(v, i, a) { return a.indexOf(v) === i; });
2057
+ // Drop "Get node" when it appears alongside a more specific operation — it's just boilerplate setup
2058
+ var meaningful = unique.length > 1
2059
+ ? unique.filter(function(v) { return v !== 'Get node'; })
2060
+ : unique;
2061
+ return truncate(meaningful.join(', '));
2062
+ }
2063
+ // No recognisable Figma API pattern — prefix with <Code> so engineers can spot it, then show first line for context
2064
+ var lines = params.code.split('\n');
2065
+ for (var j = 0; j < lines.length; j++) {
2066
+ var line = lines[j].trim();
2067
+ if (line && line.indexOf('//') !== 0 && line.indexOf('/*') !== 0) {
2068
+ return truncate('<Code> ' + line);
2069
+ }
2070
+ }
2071
+ return '<Code>';
2072
+ }
2073
+ if (COMMAND_LABELS.hasOwnProperty(type)) return COMMAND_LABELS[type];
2074
+ // Fallback: turn "RENAME_VARIABLE" into "Rename variable"
2075
+ return type.replace(/_/g, ' ').toLowerCase().replace(/^\w/, function(c) { return c.toUpperCase(); });
2076
+ }
2077
+
2078
+ // Wrap sendPluginCommand so every sent command is logged with a human summary and duration.
2079
+ var _origSendPluginCommand = window.sendPluginCommand;
2080
+ window.sendPluginCommand = function(type, params, timeoutMs) {
2081
+ var summary = summariseCommand(type, params);
2082
+ var durEntry = null;
2083
+ var histIdx = -1;
2084
+ if (summary) {
2085
+ _lastActionAt = Date.now();
2086
+ logWithHistory(summary, 'info');
2087
+ histIdx = logHistory.length - 1;
2088
+ ensureLogRefs();
2089
+ durEntry = logEntriesEl ? logEntriesEl.lastElementChild : null;
2090
+ }
2091
+ var t0 = Date.now();
2092
+ return _origSendPluginCommand(type, params, timeoutMs).then(
2093
+ function(result) {
2094
+ if (durEntry) {
2095
+ var d = durEntry.querySelector('.log-dur');
2096
+ if (d) d.textContent = (Date.now() - t0) + 'ms';
2097
+ if (result && result.success === false) {
2098
+ durEntry.className = durEntry.className.replace(/\b(info|success|warn)\b/, 'error');
2099
+ durEntry.dataset.logLevel = 'error';
2100
+ var m = durEntry.querySelector('.log-msg');
2101
+ if (m && m.textContent.indexOf('[!]') !== 0) m.textContent = '[!] ' + m.textContent;
2102
+ if (histIdx >= 0 && logHistory[histIdx]) {
2103
+ logHistory[histIdx].level = 'error';
2104
+ }
2105
+ }
2106
+ }
2107
+ return result;
2108
+ },
2109
+ function(err) {
2110
+ if (durEntry) {
2111
+ var d = durEntry.querySelector('.log-dur');
2112
+ if (d) d.textContent = (Date.now() - t0) + 'ms';
2113
+ durEntry.className = durEntry.className.replace(/\b(info|success|warn)\b/, 'error');
2114
+ durEntry.dataset.logLevel = 'error';
2115
+ var m = durEntry.querySelector('.log-msg');
2116
+ if (m && m.textContent.indexOf('[!]') !== 0) m.textContent = '[!] ' + m.textContent;
2117
+ if (histIdx >= 0 && logHistory[histIdx]) {
2118
+ logHistory[histIdx].level = 'error';
2119
+ logHistory[histIdx].message = '[!] ' + logHistory[histIdx].message;
2120
+ }
2121
+ }
2122
+ throw err;
2123
+ }
2124
+ );
2125
+ };
2126
+
2127
+ // Plugin update banner — shown when the server pushes
2128
+ // PLUGIN_UPDATE_AVAILABLE over the WebSocket. Dismiss sticks for the
2129
+ // session so repeated pushes don't nag.
2130
+ var _updateBannerDismissed = false;
2131
+ function showUpdateBanner() {
2132
+ if (_updateBannerDismissed) return;
2133
+ var b = document.getElementById('update-banner');
2134
+ if (b && !b.classList.contains('visible')) {
2135
+ b.classList.add('visible');
2136
+ autoResize();
2137
+ }
2138
+ }
2139
+ function dismissUpdateBanner() {
2140
+ _updateBannerDismissed = true;
2141
+ var b = document.getElementById('update-banner');
2142
+ if (b && b.classList.contains('visible')) {
2143
+ b.classList.remove('visible');
2144
+ autoResize();
2145
+ }
2146
+ }
2147
+
2148
+ // True while a cloud relay connection is live in the shared pool.
2149
+ function hasLiveCloudConnection() {
2150
+ var conns = (typeof window.__wsGetActiveConnections === 'function') ? (window.__wsGetActiveConnections() || []) : [];
2151
+ return conns.some(function(c) { return c && c.port === 'cloud' && c.ws && c.ws.readyState === 1; });
2152
+ }
2153
+
2154
+ // Real Pause / Resume: toggles the local WebSocket bridge.
2155
+ function setCtaState(state) {
2156
+ ensureLogRefs();
2157
+ if (!_ctaBtn) return;
2158
+ _ctaBtn.style.display = ''; // markup starts it hidden until first honest state
2159
+ if (state === 'on') { _ctaBtn.textContent = 'Pause'; _ctaBtn.disabled = false; }
2160
+ else if (state === 'paused') { _ctaBtn.textContent = 'Resume'; _ctaBtn.disabled = false; }
2161
+ else if (state === 'reconnect'){ _ctaBtn.textContent = 'Try again'; _ctaBtn.disabled = false; }
2162
+ else if (state === 'scanning') { _ctaBtn.textContent = 'Connecting…'; _ctaBtn.disabled = true; }
2163
+ }
2164
+
2165
+ function toggleLocalConnection() {
2166
+ ensureLogRefs();
2167
+ if (!_ctaBtn) return;
2168
+ var label = _ctaBtn.textContent;
2169
+ if (label === 'Pause') {
2170
+ // Remember a live cloud connection so Resume can restore it.
2171
+ _cloudWasConnectedBeforePause = hasLiveCloudConnection();
2172
+ if (typeof window.__wsDisconnectAll === 'function') window.__wsDisconnectAll();
2173
+ setCtaState('paused');
2174
+ deriveAndRenderStatus();
2175
+ logWithHistory('Paused', 'warn');
2176
+ } else if (label === 'Resume' || label === 'Try again') {
2177
+ setCtaState('scanning');
2178
+ logWithHistory('Looking for your AI app…', 'info');
2179
+ if (typeof window.__wsManualScan === 'function') window.__wsManualScan();
2180
+ else if (typeof window.__wsScanAndConnect === 'function') window.__wsScanAndConnect();
2181
+ // Re-establish the cloud connection if one existed before Pause
2182
+ // (the scan above only covers localhost ports).
2183
+ if (_cloudWasConnectedBeforePause) {
2184
+ _cloudWasConnectedBeforePause = false;
2185
+ if (_lastCloudCode && !_cloudUserDisconnected) {
2186
+ _cloudRetryCount = 0;
2187
+ cloudDial(_lastCloudCode, true);
2188
+ }
2189
+ }
2190
+ deriveAndRenderStatus();
2191
+ // Watch for connection success or scan timeout.
2192
+ var attempts = 0;
2193
+ var poller = setInterval(function() {
2194
+ attempts++;
2195
+ var n = window.__wsGetActiveConnections ? window.__wsGetActiveConnections().length : 0;
2196
+ if (n > 0) {
2197
+ clearInterval(poller);
2198
+ setCtaState('on');
2199
+ deriveAndRenderStatus();
2200
+ } else if (attempts > 20) { // ~10s max
2201
+ clearInterval(poller);
2202
+ setCtaState('reconnect');
2203
+ deriveAndRenderStatus();
2204
+ logWithHistory('Couldn\'t find your AI app. Open Claude (or your AI assistant), then press Try again.', 'error');
2205
+ }
2206
+ }, 500);
2207
+ }
2208
+ }
2209
+
2210
+ // Keep the CTA button honest about the real connection state: a
2211
+ // never-connected or dropped plugin shows a clickable "Try again" instead
2212
+ // of a misleading "Pause". Skips while a scan is mid-flight (button
2213
+ // disabled or __wsIsScanning) to avoid fighting the transient click state.
2214
+ function reconcileCta() {
2215
+ ensureLogRefs();
2216
+ if (!_ctaBtn) return;
2217
+ if (_ctaBtn.disabled) return;
2218
+ if (typeof window.__wsIsScanning === 'function' && window.__wsIsScanning()) return;
2219
+ var n = (typeof window.__wsGetActiveConnections === 'function') ? window.__wsGetActiveConnections().length : 0;
2220
+ var paused = (typeof window.__wsIsPaused === 'function') ? window.__wsIsPaused() : false;
2221
+ if (paused) {
2222
+ if (_ctaBtn.textContent !== 'Resume') setCtaState('paused');
2223
+ } else if (n > 0) {
2224
+ if (_ctaBtn.textContent !== 'Pause') setCtaState('on');
2225
+ } else {
2226
+ if (_ctaBtn.textContent !== 'Try again') setCtaState('reconnect');
2227
+ }
2228
+ // First reconcile also unhides the button with its honest label.
2229
+ if (_ctaBtn.style.display === 'none') _ctaBtn.style.display = '';
2230
+ }
2231
+
2232
+ // Periodic reconcile of CTA + status pill/meta (cheap, <1ms). Also
2233
+ // refreshes "Last action: Xs ago" via renderStatusMeta inside
2234
+ // deriveAndRenderStatus.
2235
+ setInterval(function() {
2236
+ reconcileCta();
2237
+ deriveAndRenderStatus();
2238
+ }, 2000);
2239
+
2240
+ // One-shot on first ready: render honest initial state and capture the
2241
+ // plugin's own version (used in the Activity export header) without
2242
+ // logging a command line — hence the unwrapped sender. The version is
2243
+ // read from the GET_FILE_INFO_RESULT handler below.
2244
+ setTimeout(function() {
2245
+ reconcileCta();
2246
+ deriveAndRenderStatus();
2247
+ _origSendPluginCommand('GET_FILE_INFO', {}).catch(function() { /* ignore */ });
2248
+ }, 500);
2249
+
2250
+ // Size the plugin window to fit content on first paint.
2251
+ requestAnimationFrame(autoResize);
2252
+
2253
+ // Single source of truth for the cloud pairing status line — derives the
2254
+ // text from actual state (same philosophy as deriveAndRenderStatus for the
2255
+ // pill) instead of trusting one-off events. Every string is prefixed
2256
+ // "Cloud" so it can never read as the plugin's overall status, and the
2257
+ // line stays empty (hidden via :not(:empty)) unless it has something
2258
+ // useful to say — a background failure like an expired pairing code
2259
+ // restored from a previous session stays silent. Transient messages
2260
+ // ("Cloud: connecting…", bad-code errors) are written directly by
2261
+ // cloudDial/cloudConnect and survive until the next state change.
2262
+ function renderCloudStatus() {
2263
+ var statusEl = document.getElementById('cloud-status');
2264
+ if (!statusEl) return;
2265
+ var cp = document.getElementById('cloud-pair');
2266
+ var panelOpen = !!(cp && cp.classList.contains('visible'));
2267
+ var paused = (typeof window.__wsIsPaused === 'function') ? window.__wsIsPaused() : false;
2268
+
2269
+ if (paused) {
2270
+ // Pause closes the cloud socket deliberately and Resume redials it —
2271
+ // never present that as a lost pairing (retry is suppressed while
2272
+ // paused, so "reconnecting…" would also be untrue).
2273
+ statusEl.textContent = panelOpen ? 'Cloud: paused' : '';
2274
+ statusEl.className = 'cloud-status';
2275
+ } else if (hasLiveCloudConnection()) {
2276
+ statusEl.textContent = panelOpen ? 'Cloud: connected' : '';
2277
+ statusEl.className = 'cloud-status connected';
2278
+ } else if (!_cloudUserDisconnected && _cloudEverConnected && _cloudRetryCount < CLOUD_MAX_RETRIES) {
2279
+ // Paired earlier this session and dropped — actionable, so it shows
2280
+ // even while the panel is collapsed.
2281
+ statusEl.textContent = 'Cloud pairing lost — reconnecting…';
2282
+ statusEl.className = 'cloud-status';
2283
+ } else if (!_cloudUserDisconnected && _cloudEverConnected) {
2284
+ statusEl.textContent = 'Cloud pairing lost. Generate a fresh code in Claude and reconnect.';
2285
+ statusEl.className = 'cloud-status error';
2286
+ } else {
2287
+ statusEl.textContent = panelOpen ? 'Cloud: not connected' : '';
2288
+ statusEl.className = 'cloud-status';
2289
+ }
2290
+ autoResize();
2291
+ }
2292
+
2293
+ function resetCloudUI() {
2294
+ var btn = document.getElementById('cloud-btn');
2295
+ renderCloudStatus();
2296
+ if (btn) {
2297
+ btn.disabled = false;
2298
+ btn.textContent = 'Connect';
2299
+ btn.onclick = cloudConnect;
2300
+ }
2301
+ cloudWs = null;
2302
+ }
2303
+
2304
+ // Shared dialer used by manual Connect, auto-reconnect after a drop,
2305
+ // restored-config auto-connect, and Resume-after-Pause. `isAuto` softens
2306
+ // the error UX for background attempts and arms the bounded retry chain.
2307
+ function cloudDial(code, isAuto) {
2308
+ var btn = document.getElementById('cloud-btn');
2309
+ var statusEl = document.getElementById('cloud-status');
2310
+
2311
+ // Close existing cloud connection if any (manual reason suppresses
2312
+ // the pool's same-port retry for the old socket).
2313
+ if (cloudWs && cloudWs.readyState <= 1) {
2314
+ try { cloudWs.close(1000, 'Manual disconnect'); } catch (e) {}
2315
+ }
2316
+
2317
+ if (btn) btn.disabled = true;
2318
+ // Manual attempts show progress; auto re-dials render the derived state
2319
+ // ("Cloud pairing lost — reconnecting…", or silence for a background
2320
+ // restore) so an unlabeled transient never floats under the main pill.
2321
+ if (isAuto) {
2322
+ renderCloudStatus();
2323
+ } else if (statusEl) {
2324
+ statusEl.textContent = 'Cloud: connecting…';
2325
+ statusEl.className = 'cloud-status';
2326
+ }
2327
+
2328
+ try {
2329
+ var opened = false;
2330
+ var sawError = false;
2331
+ cloudWs = new WebSocket(CLOUD_RELAY_HOST + '/ws/pair?code=' + code);
2332
+
2333
+ cloudWs.onopen = function() {
2334
+ opened = true;
2335
+ _lastCloudCode = code;
2336
+ _cloudUserDisconnected = false;
2337
+ _cloudRetryCount = 0;
2338
+ _cloudEverConnected = true;
2339
+ if (btn) { btn.disabled = false; btn.textContent = 'Disconnect'; btn.onclick = cloudDisconnect; }
2340
+
2341
+ // Add to the shared connection pool (uses same handlers as localhost).
2342
+ // Pass resetCloudUI as disconnect callback — attachWsHandlers overwrites
2343
+ // onclose, so this callback ensures cloud UI resets on server-initiated close.
2344
+ if (window.__wsAddCloudConnection) {
2345
+ window.__wsAddCloudConnection(cloudWs, 'cloud', resetCloudUI);
2346
+ }
2347
+
2348
+ // After the pool add, so hasLiveCloudConnection() sees this socket.
2349
+ renderCloudStatus();
2350
+
2351
+ // Persist cloud config via code.js clientStorage
2352
+ parent.postMessage({ pluginMessage: {
2353
+ type: 'STORE_CLOUD_CONFIG',
2354
+ code: code
2355
+ }}, '*');
2356
+ };
2357
+
2358
+ cloudWs.onerror = function() {
2359
+ sawError = true;
2360
+ if (statusEl && !isAuto) {
2361
+ statusEl.textContent = 'That code didn\'t work. Ask Claude for a fresh pairing code and try again.';
2362
+ statusEl.className = 'cloud-status error';
2363
+ }
2364
+ if (btn) btn.disabled = false;
2365
+ };
2366
+
2367
+ // Note: onclose here handles pre-connection close (e.g., bad code).
2368
+ // After onopen, attachWsHandlers overwrites this — resetCloudUI callback
2369
+ // handles post-connection close instead.
2370
+ cloudWs.onclose = function(event) {
2371
+ if (!opened && sawError && !isAuto) {
2372
+ // Keep the visible error message (onclose fires right after
2373
+ // onerror and would otherwise clobber it); restore the button.
2374
+ if (btn) { btn.disabled = false; btn.textContent = 'Connect'; btn.onclick = cloudConnect; }
2375
+ cloudWs = null;
2376
+ } else {
2377
+ resetCloudUI();
2378
+ }
2379
+ // Auto attempt that never opened → keep trying within the budget.
2380
+ if (isAuto && !opened) scheduleCloudRetry();
2381
+ };
2382
+ } catch (e) {
2383
+ if (statusEl) { statusEl.textContent = 'Failed: ' + e.message; statusEl.className = 'cloud-status error'; }
2384
+ if (btn) btn.disabled = false;
2385
+ }
2386
+ }
2387
+
2388
+ // Bounded auto-reconnect with backoff. Gives up after CLOUD_MAX_RETRIES,
2389
+ // never fires while paused or after a deliberate user Disconnect.
2390
+ function scheduleCloudRetry() {
2391
+ if (_cloudUserDisconnected || !_lastCloudCode) return;
2392
+ if (typeof window.__wsIsPaused === 'function' && window.__wsIsPaused()) return;
2393
+ if (_cloudRetryCount >= CLOUD_MAX_RETRIES) return;
2394
+ _cloudRetryCount++;
2395
+ setTimeout(function() {
2396
+ if (_cloudUserDisconnected || hasLiveCloudConnection()) return;
2397
+ if (typeof window.__wsIsPaused === 'function' && window.__wsIsPaused()) return;
2398
+ cloudDial(_lastCloudCode, true);
2399
+ }, Math.min(2000 * _cloudRetryCount, 10000));
2400
+ }
2401
+
2402
+ // Called by the connection pool when the pooled cloud connection drops —
2403
+ // re-dials the actual relay URL instead of 'ws://localhost:cloud'.
2404
+ window.__cloudReconnect = function() {
2405
+ scheduleCloudRetry();
2406
+ };
2407
+
2408
+ function cloudConnect() {
2409
+ var codeInput = document.getElementById('cloud-code');
2410
+ var statusEl = document.getElementById('cloud-status');
2411
+ var code = (codeInput.value || '').trim().toUpperCase();
2412
+
2413
+ if (!code || code.length < 4) {
2414
+ statusEl.textContent = 'Enter pairing code';
2415
+ statusEl.className = 'cloud-status error';
2416
+ return;
2417
+ }
2418
+
2419
+ _cloudUserDisconnected = false;
2420
+ _cloudRetryCount = 0;
2421
+ cloudDial(code, false);
2422
+ }
2423
+
2424
+ function cloudDisconnect() {
2425
+ _cloudUserDisconnected = true; // deliberate — suppress auto re-dial
2426
+ _cloudEverConnected = false; // a deliberate disconnect is not a "lost" pairing
2427
+ if (cloudWs) {
2428
+ try { cloudWs.close(1000, 'Manual disconnect'); } catch (e) {}
2429
+ }
2430
+ // Reset UI immediately — don't rely on onclose (may be overwritten)
2431
+ resetCloudUI();
2432
+ }
2433
+
2434
+ // ============================================================================
2435
+ // MESSAGE HANDLER - Process responses from plugin worker
2436
+ // ============================================================================
2437
+ window.onmessage = (event) => {
2438
+ const msg = event.data.pluginMessage;
2439
+ if (!msg) return;
2440
+
2441
+ // Generic result handler
2442
+ const handleResult = (resultType, dataKey) => {
2443
+ const request = window.__figmaPendingRequests.get(msg.requestId);
2444
+ if (request) {
2445
+ if (request.timeoutId) clearTimeout(request.timeoutId);
2446
+ if (msg.success) {
2447
+ var result = { success: true };
2448
+ if (dataKey && msg[dataKey] !== undefined) result[dataKey] = msg[dataKey];
2449
+ if (msg.data !== undefined) result.data = msg.data;
2450
+ if (msg.oldName !== undefined) result.oldName = msg.oldName;
2451
+ if (msg.instance !== undefined) result.instance = msg.instance;
2452
+ if (msg.slot !== undefined) result.slot = msg.slot;
2453
+ if (msg.warnings !== undefined) result.warnings = msg.warnings;
2454
+ if (msg.updatedCount !== undefined) result.updatedCount = msg.updatedCount;
2455
+ if (msg.nodes !== undefined) result.nodes = msg.nodes;
2456
+ request.resolve(result);
2457
+ } else {
2458
+ request.resolve({ success: false, error: msg.error || 'Unknown error' });
2459
+ }
2460
+ window.__figmaPendingRequests.delete(msg.requestId);
2461
+ }
2462
+ };
2463
+
2464
+ // Handle message types
2465
+ switch (msg.type) {
2466
+ case 'VARIABLES_DATA':
2467
+ window.__figmaVariablesData = msg.data;
2468
+ window.__figmaVariablesReady = true;
2469
+ // A Figma-side data event says nothing about the WS connection —
2470
+ // record the fact and let the derived state decide the pill.
2471
+ bridgeError = false;
2472
+ deriveAndRenderStatus();
2473
+ console.log('[MCP Bridge] Active - ' + (msg.data.variables?.length || 0) + ' vars');
2474
+ // Forward to WebSocket client if connected
2475
+ if (window.__wsForwardVariables) window.__wsForwardVariables(msg.data);
2476
+ break;
2477
+
2478
+ case 'COMPONENT_DATA':
2479
+ window.__figmaComponentData = msg.data;
2480
+ var req = window.__figmaComponentRequests.get(msg.requestId);
2481
+ if (req) { req.resolve(msg.data); window.__figmaComponentRequests.delete(msg.requestId); }
2482
+ break;
2483
+
2484
+ case 'COMPONENT_ERROR':
2485
+ var req2 = window.__figmaComponentRequests.get(msg.requestId);
2486
+ if (req2) { req2.reject(new Error(msg.error)); window.__figmaComponentRequests.delete(msg.requestId); }
2487
+ break;
2488
+
2489
+ case 'ERROR':
2490
+ window.__figmaVariablesReady = false;
2491
+ bridgeError = true; // pill shows "Something broke" + reopen hint
2492
+ deriveAndRenderStatus();
2493
+ console.error('[MCP Bridge] Error:', msg.error);
2494
+ break;
2495
+
2496
+ // Variable operations
2497
+ case 'EXECUTE_CODE_RESULT':
2498
+ handleResult('EXECUTE_CODE', 'result');
2499
+ break;
2500
+ case 'UPDATE_VARIABLE_RESULT':
2501
+ handleResult('UPDATE_VARIABLE', 'variable');
2502
+ break;
2503
+ case 'CREATE_VARIABLE_RESULT':
2504
+ handleResult('CREATE_VARIABLE', 'variable');
2505
+ break;
2506
+ case 'CREATE_VARIABLE_COLLECTION_RESULT':
2507
+ handleResult('CREATE_VARIABLE_COLLECTION', 'collection');
2508
+ break;
2509
+ case 'DELETE_VARIABLE_RESULT':
2510
+ handleResult('DELETE_VARIABLE', 'deleted');
2511
+ break;
2512
+ case 'DELETE_VARIABLE_COLLECTION_RESULT':
2513
+ handleResult('DELETE_VARIABLE_COLLECTION', 'deleted');
2514
+ break;
2515
+ case 'REFRESH_VARIABLES_RESULT':
2516
+ handleResult('REFRESH_VARIABLES', null);
2517
+ break;
2518
+ case 'RENAME_VARIABLE_RESULT':
2519
+ handleResult('RENAME_VARIABLE', 'variable');
2520
+ break;
2521
+ case 'SET_VARIABLE_DESCRIPTION_RESULT':
2522
+ handleResult('SET_VARIABLE_DESCRIPTION', 'variable');
2523
+ break;
2524
+ case 'ADD_MODE_RESULT':
2525
+ handleResult('ADD_MODE', 'collection');
2526
+ break;
2527
+ case 'RENAME_MODE_RESULT':
2528
+ handleResult('RENAME_MODE', 'collection');
2529
+ break;
2530
+ case 'GET_LOCAL_COMPONENTS_RESULT':
2531
+ handleResult('GET_LOCAL_COMPONENTS', null);
2532
+ break;
2533
+ case 'INSTANTIATE_COMPONENT_RESULT':
2534
+ handleResult('INSTANTIATE_COMPONENT', 'instance');
2535
+ break;
2536
+ // Component set creation (payload flows via msg.data — already on the
2537
+ // handleResult whitelist, so no new top-level fields to forward)
2538
+ case 'CREATE_COMPONENT_SET_RESULT':
2539
+ handleResult('CREATE_COMPONENT_SET', null);
2540
+ break;
2541
+
2542
+ // NEW: Component property operations
2543
+ case 'SET_NODE_DESCRIPTION_RESULT':
2544
+ handleResult('SET_NODE_DESCRIPTION', 'node');
2545
+ break;
2546
+ case 'ADD_COMPONENT_PROPERTY_RESULT':
2547
+ handleResult('ADD_COMPONENT_PROPERTY', 'propertyName');
2548
+ break;
2549
+ case 'EDIT_COMPONENT_PROPERTY_RESULT':
2550
+ handleResult('EDIT_COMPONENT_PROPERTY', 'propertyName');
2551
+ break;
2552
+ case 'DELETE_COMPONENT_PROPERTY_RESULT':
2553
+ handleResult('DELETE_COMPONENT_PROPERTY', null);
2554
+ break;
2555
+ case 'CREATE_SLOT_RESULT':
2556
+ handleResult('CREATE_SLOT', 'slot');
2557
+ break;
2558
+ case 'GET_SLOTS_RESULT':
2559
+ handleResult('GET_SLOTS', null);
2560
+ break;
2561
+ case 'APPEND_TO_SLOT_RESULT':
2562
+ handleResult('APPEND_TO_SLOT', 'appendedNode');
2563
+ break;
2564
+ case 'RESET_SLOT_RESULT':
2565
+ handleResult('RESET_SLOT', 'slot');
2566
+ break;
2567
+
2568
+ // NEW: Node manipulation operations
2569
+ case 'RESIZE_NODE_RESULT':
2570
+ handleResult('RESIZE_NODE', 'node');
2571
+ break;
2572
+ case 'MOVE_NODE_RESULT':
2573
+ handleResult('MOVE_NODE', 'node');
2574
+ break;
2575
+ case 'SET_NODE_FILLS_RESULT':
2576
+ handleResult('SET_NODE_FILLS', 'node');
2577
+ break;
2578
+ case 'SET_NODE_STROKES_RESULT':
2579
+ handleResult('SET_NODE_STROKES', 'node');
2580
+ break;
2581
+ case 'SET_NODE_OPACITY_RESULT':
2582
+ handleResult('SET_NODE_OPACITY', 'node');
2583
+ break;
2584
+ case 'SET_NODE_CORNER_RADIUS_RESULT':
2585
+ handleResult('SET_NODE_CORNER_RADIUS', 'node');
2586
+ break;
2587
+ case 'CLONE_NODE_RESULT':
2588
+ handleResult('CLONE_NODE', 'node');
2589
+ break;
2590
+ case 'DELETE_NODE_RESULT':
2591
+ handleResult('DELETE_NODE', 'deleted');
2592
+ break;
2593
+ case 'RENAME_NODE_RESULT':
2594
+ handleResult('RENAME_NODE', 'node');
2595
+ break;
2596
+ case 'SET_TEXT_CONTENT_RESULT':
2597
+ handleResult('SET_TEXT_CONTENT', 'node');
2598
+ break;
2599
+ case 'CREATE_CHILD_NODE_RESULT':
2600
+ handleResult('CREATE_CHILD_NODE', 'node');
2601
+ break;
2602
+
2603
+ // NEW: Screenshot and instance properties (visual validation loop fix)
2604
+ case 'CAPTURE_SCREENSHOT_RESULT':
2605
+ handleResult('CAPTURE_SCREENSHOT', 'image');
2606
+ break;
2607
+ case 'SET_IMAGE_FILL_RESULT':
2608
+ handleResult('SET_IMAGE_FILL', 'imageHash');
2609
+ break;
2610
+ case 'SET_INSTANCE_PROPERTIES_RESULT':
2611
+ handleResult('SET_INSTANCE_PROPERTIES', 'instance');
2612
+ break;
2613
+ case 'LINT_DESIGN_RESULT':
2614
+ handleResult('LINT_DESIGN', 'data');
2615
+ break;
2616
+ case 'AUDIT_COMPONENT_ACCESSIBILITY_RESULT':
2617
+ handleResult('AUDIT_COMPONENT_ACCESSIBILITY', 'data');
2618
+ break;
2619
+
2620
+ // FigJam tools
2621
+ case 'CREATE_STICKY_RESULT':
2622
+ handleResult('CREATE_STICKY', 'data');
2623
+ break;
2624
+ case 'CREATE_STICKIES_RESULT':
2625
+ handleResult('CREATE_STICKIES', 'data');
2626
+ break;
2627
+ case 'CREATE_CONNECTOR_RESULT':
2628
+ handleResult('CREATE_CONNECTOR', 'data');
2629
+ break;
2630
+ case 'CREATE_SHAPE_WITH_TEXT_RESULT':
2631
+ handleResult('CREATE_SHAPE_WITH_TEXT', 'data');
2632
+ break;
2633
+ case 'CREATE_SECTION_RESULT':
2634
+ handleResult('CREATE_SECTION', 'data');
2635
+ break;
2636
+ case 'CREATE_TABLE_RESULT':
2637
+ handleResult('CREATE_TABLE', 'data');
2638
+ break;
2639
+ case 'CREATE_CODE_BLOCK_RESULT':
2640
+ handleResult('CREATE_CODE_BLOCK', 'data');
2641
+ break;
2642
+ case 'GET_BOARD_CONTENTS_RESULT':
2643
+ handleResult('GET_BOARD_CONTENTS', 'data');
2644
+ break;
2645
+ case 'GET_CONNECTIONS_RESULT':
2646
+ handleResult('GET_CONNECTIONS', 'data');
2647
+ break;
2648
+
2649
+ // Slides tools
2650
+ case 'LIST_SLIDES_RESULT':
2651
+ handleResult('LIST_SLIDES', 'data');
2652
+ break;
2653
+ case 'GET_SLIDE_CONTENT_RESULT':
2654
+ handleResult('GET_SLIDE_CONTENT', 'data');
2655
+ break;
2656
+ case 'CREATE_SLIDE_RESULT':
2657
+ handleResult('CREATE_SLIDE', 'data');
2658
+ break;
2659
+ case 'DELETE_SLIDE_RESULT':
2660
+ handleResult('DELETE_SLIDE', 'data');
2661
+ break;
2662
+ case 'DUPLICATE_SLIDE_RESULT':
2663
+ handleResult('DUPLICATE_SLIDE', 'data');
2664
+ break;
2665
+ case 'GET_SLIDE_GRID_RESULT':
2666
+ handleResult('GET_SLIDE_GRID', 'data');
2667
+ break;
2668
+ case 'REORDER_SLIDES_RESULT':
2669
+ handleResult('REORDER_SLIDES', 'data');
2670
+ break;
2671
+ case 'SET_SLIDE_TRANSITION_RESULT':
2672
+ handleResult('SET_SLIDE_TRANSITION', 'data');
2673
+ break;
2674
+ case 'GET_SLIDE_TRANSITION_RESULT':
2675
+ handleResult('GET_SLIDE_TRANSITION', 'data');
2676
+ break;
2677
+ case 'SET_SLIDES_VIEW_MODE_RESULT':
2678
+ handleResult('SET_SLIDES_VIEW_MODE', 'data');
2679
+ break;
2680
+ case 'GET_FOCUSED_SLIDE_RESULT':
2681
+ handleResult('GET_FOCUSED_SLIDE', 'data');
2682
+ break;
2683
+ case 'FOCUS_SLIDE_RESULT':
2684
+ handleResult('FOCUS_SLIDE', 'data');
2685
+ break;
2686
+ case 'SKIP_SLIDE_RESULT':
2687
+ handleResult('SKIP_SLIDE', 'data');
2688
+ break;
2689
+ case 'GET_TEXT_STYLES_RESULT':
2690
+ handleResult('GET_TEXT_STYLES', 'data');
2691
+ break;
2692
+ case 'SET_SLIDE_BACKGROUND_RESULT':
2693
+ handleResult('SET_SLIDE_BACKGROUND', 'data');
2694
+ break;
2695
+ case 'ADD_TEXT_TO_SLIDE_RESULT':
2696
+ handleResult('ADD_TEXT_TO_SLIDE', 'data');
2697
+ break;
2698
+ case 'ADD_SHAPE_TO_SLIDE_RESULT':
2699
+ handleResult('ADD_SHAPE_TO_SLIDE', 'data');
2700
+ break;
2701
+
2702
+ // File info
2703
+ case 'GET_FILE_INFO_RESULT':
2704
+ // Capture the plugin's own version for the Activity export header
2705
+ // (moved here from the deleted Info panel so any GET_FILE_INFO
2706
+ // response keeps it fresh, regardless of who asked).
2707
+ var fileInfo = msg.fileInfo || msg.data;
2708
+ if (fileInfo && fileInfo.pluginVersion) {
2709
+ PLUGIN_VERSION = 'v' + fileInfo.pluginVersion;
2710
+ }
2711
+ handleResult('GET_FILE_INFO', 'fileInfo');
2712
+ break;
2713
+
2714
+ // Cloud config restored from clientStorage by code.js — auto-fill the
2715
+ // pairing input and reconnect without making the user re-pair.
2716
+ case 'CLOUD_CONFIG_RESTORED':
2717
+ if (msg.config && msg.config.code && !_cloudUserDisconnected) {
2718
+ var restoredCode = String(msg.config.code).trim().toUpperCase();
2719
+ var restoredInput = document.getElementById('cloud-code');
2720
+ if (restoredInput) restoredInput.value = restoredCode;
2721
+ if (restoredCode.length >= 4 && !hasLiveCloudConnection()) {
2722
+ _lastCloudCode = restoredCode;
2723
+ _cloudRetryCount = 0;
2724
+ cloudDial(restoredCode, true);
2725
+ }
2726
+ }
2727
+ break;
2728
+
2729
+ // Plugin UI reload
2730
+ case 'RELOAD_UI_RESULT':
2731
+ handleResult('RELOAD_UI', null);
2732
+ break;
2733
+
2734
+ // Component set analysis
2735
+ case 'ANALYZE_COMPONENT_SET_RESULT':
2736
+ handleResult('ANALYZE_COMPONENT_SET', null);
2737
+ break;
2738
+
2739
+ // Deep component extraction
2740
+ case 'DEEP_GET_COMPONENT_RESULT':
2741
+ handleResult('DEEP_GET_COMPONENT', null);
2742
+ break;
2743
+
2744
+ // Annotation tools (data flows via msg.data, no top-level dataKey needed)
2745
+ case 'GET_ANNOTATIONS_RESULT':
2746
+ handleResult('GET_ANNOTATIONS', null);
2747
+ break;
2748
+ case 'SET_ANNOTATIONS_RESULT':
2749
+ handleResult('SET_ANNOTATIONS', null);
2750
+ break;
2751
+ case 'GET_ANNOTATION_CATEGORIES_RESULT':
2752
+ handleResult('GET_ANNOTATION_CATEGORIES', null);
2753
+ break;
2754
+
2755
+ // Document change events (for cache invalidation via WebSocket)
2756
+ case 'DOCUMENT_CHANGE':
2757
+ if (window.__wsForwardDocumentChange) window.__wsForwardDocumentChange(msg.data);
2758
+ break;
2759
+
2760
+ // v1.25.0: metadata change events (description/annotation edits via Plugin API)
2761
+ case 'METADATA_CHANGE':
2762
+ if (window.__wsForwardMetadataChange) window.__wsForwardMetadataChange(msg.data);
2763
+ break;
2764
+
2765
+ // Console capture events (for console monitoring via WebSocket)
2766
+ case 'CONSOLE_CAPTURE':
2767
+ if (window.__wsForwardConsoleCapture) window.__wsForwardConsoleCapture(msg);
2768
+ break;
2769
+
2770
+ // Selection change events (for selection tracking via WebSocket)
2771
+ case 'SELECTION_CHANGE':
2772
+ if (window.__wsForwardSelectionChange) window.__wsForwardSelectionChange(msg.data);
2773
+ break;
2774
+
2775
+ // Page change events (for page tracking via WebSocket)
2776
+ case 'PAGE_CHANGE':
2777
+ if (window.__wsForwardPageChange) window.__wsForwardPageChange(msg.data);
2778
+ break;
2779
+ }
2780
+ };
2781
+ </script>
2782
+ </body>
2783
+ </html>