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