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