@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,2751 @@
1
+ /**
2
+ * Design-Code Parity Checker & Documentation Generator
3
+ * MCP tools for comparing Figma design specs with code-side data
4
+ * and generating platform-agnostic component documentation.
5
+ */
6
+ import { z } from "zod";
7
+ import { extractFileKey } from "./figma-api.js";
8
+ import { createChildLogger } from "./logger.js";
9
+ import { EnrichmentService } from "./enrichment/index.js";
10
+ const logger = createChildLogger({ component: "design-code-tools" });
11
+ const enrichmentService = new EnrichmentService(logger);
12
+ // ============================================================================
13
+ // Shared Helpers
14
+ // ============================================================================
15
+ // Re-exported from the shared diff module so existing imports continue to work.
16
+ export { figmaRGBAToHex, normalizeColor, numericClose } from "./diff/property-compare.js";
17
+ import { figmaRGBAToHex, normalizeColor, numericClose } from "./diff/property-compare.js";
18
+ /** Calculate parity score from discrepancy counts */
19
+ export function calculateParityScore(critical, major, minor, info) {
20
+ return Math.max(0, 100 - (critical * 15 + major * 8 + minor * 3 + info * 1));
21
+ }
22
+ /** Extract first solid fill color from Figma node */
23
+ function extractFirstFillColor(fills) {
24
+ if (!fills || !Array.isArray(fills))
25
+ return null;
26
+ const solid = fills.find((f) => f.type === "SOLID" && f.visible !== false);
27
+ if (!solid?.color)
28
+ return null;
29
+ return figmaRGBAToHex({ ...solid.color, a: solid.opacity ?? solid.color.a ?? 1 });
30
+ }
31
+ /** Extract first stroke color from Figma node */
32
+ function extractFirstStrokeColor(strokes) {
33
+ if (!strokes || !Array.isArray(strokes))
34
+ return null;
35
+ const solid = strokes.find((s) => s.type === "SOLID" && s.visible !== false);
36
+ if (!solid?.color)
37
+ return null;
38
+ return figmaRGBAToHex({ ...solid.color, a: solid.opacity ?? solid.color.a ?? 1 });
39
+ }
40
+ /** Extract text style properties from a Figma text node */
41
+ function extractTextProperties(node) {
42
+ const style = node.style || {};
43
+ const result = {};
44
+ if (style.fontFamily)
45
+ result.fontFamily = style.fontFamily;
46
+ if (style.fontSize)
47
+ result.fontSize = style.fontSize;
48
+ if (style.fontWeight)
49
+ result.fontWeight = style.fontWeight;
50
+ if (style.lineHeightPx)
51
+ result.lineHeight = style.lineHeightPx;
52
+ if (style.letterSpacing)
53
+ result.letterSpacing = style.letterSpacing;
54
+ return result;
55
+ }
56
+ /** Extract spacing/layout properties from a Figma node */
57
+ function extractSpacingProperties(node) {
58
+ const result = {};
59
+ if (node.paddingTop !== undefined)
60
+ result.paddingTop = node.paddingTop;
61
+ if (node.paddingRight !== undefined)
62
+ result.paddingRight = node.paddingRight;
63
+ if (node.paddingBottom !== undefined)
64
+ result.paddingBottom = node.paddingBottom;
65
+ if (node.paddingLeft !== undefined)
66
+ result.paddingLeft = node.paddingLeft;
67
+ if (node.itemSpacing !== undefined)
68
+ result.gap = node.itemSpacing;
69
+ if (node.absoluteBoundingBox) {
70
+ result.width = node.absoluteBoundingBox.width;
71
+ result.height = node.absoluteBoundingBox.height;
72
+ }
73
+ return result;
74
+ }
75
+ /** Map Figma font weight number to CSS font weight */
76
+ function figmaFontWeight(weight) {
77
+ // Figma already uses numeric weights
78
+ return weight;
79
+ }
80
+ /** Split markdown by H2 headers for platforms that need chunking */
81
+ export function chunkMarkdownByHeaders(markdown) {
82
+ const chunks = [];
83
+ const lines = markdown.split("\n");
84
+ let currentHeading = "";
85
+ let currentContent = [];
86
+ for (const line of lines) {
87
+ if (line.startsWith("## ")) {
88
+ if (currentHeading || currentContent.length > 0) {
89
+ chunks.push({ heading: currentHeading, content: currentContent.join("\n").trim() });
90
+ }
91
+ currentHeading = line.replace("## ", "").trim();
92
+ currentContent = [];
93
+ }
94
+ else {
95
+ currentContent.push(line);
96
+ }
97
+ }
98
+ if (currentHeading || currentContent.length > 0) {
99
+ chunks.push({ heading: currentHeading, content: currentContent.join("\n").trim() });
100
+ }
101
+ return chunks;
102
+ }
103
+ /**
104
+ * Clean a raw Figma variant name like "Type=Image, Size=12" into "Image / 12".
105
+ * Extracts just the values from "Key=Value" pairs, joined by " / ".
106
+ */
107
+ export function cleanVariantName(rawName) {
108
+ // Match Key=Value pairs separated by comma/space
109
+ const pairs = rawName.match(/(\w[\w\s]*)=([^,]+)/g);
110
+ if (!pairs || pairs.length === 0)
111
+ return rawName;
112
+ const values = pairs.map((p) => {
113
+ const eqIdx = p.indexOf("=");
114
+ return p.slice(eqIdx + 1).trim();
115
+ });
116
+ return values.join(" / ");
117
+ }
118
+ /**
119
+ * Parse a Figma component description into structured sections.
120
+ * Handles markdown-formatted descriptions with headers, bullet points, etc.
121
+ */
122
+ export function parseComponentDescription(description) {
123
+ const result = {
124
+ overview: "",
125
+ whenToUse: [],
126
+ whenNotToUse: [],
127
+ contentGuidelines: [],
128
+ accessibilityNotes: [],
129
+ additionalNotes: [],
130
+ };
131
+ if (!description)
132
+ return result;
133
+ // Pre-process: split inline section headers onto their own lines.
134
+ // Figma sometimes concatenates headers without newlines: "...sentence.When to Use\n- bullet..."
135
+ // We detect known section header patterns appearing after sentence-ending chars (word, period, paren)
136
+ // but NOT after markdown formatting like ** or ## to avoid breaking formatted headers.
137
+ const inlineHeaderPatterns = [
138
+ /(?<=[\w.)!?])(When\s+to\s+Use)/gi,
139
+ /(?<=[\w.)!?])(When\s+NOT\s+to\s+Use)/gi,
140
+ /(?<=[\w.)!?])(When\s+to\s+Not\s+Use)/gi,
141
+ /(?<=[\w.)!?])(Don'?t\s+Use)/gi,
142
+ /(?<=[\w.)!?])(Accessibility)/gi,
143
+ /(?<=[\w.)!?])(Content\s+Requirements?)/gi,
144
+ /(?<=[\w.)!?])(Content\s+Guidelines?)/gi,
145
+ /(?<=[\w.)!?])(Writing\s+Guidelines?)/gi,
146
+ /(?<=[\w.)!?])(Variants\b)/gi,
147
+ ];
148
+ let normalized = description.replace(/\r\n/g, "\n");
149
+ for (const pattern of inlineHeaderPatterns) {
150
+ normalized = normalized.replace(pattern, "\n$1");
151
+ }
152
+ // Normalize line endings and split
153
+ const lines = normalized.split("\n");
154
+ let currentSection = "overview";
155
+ let currentContentHeading = "";
156
+ const overviewLines = [];
157
+ // Known plain-text section header patterns (for descriptions without markdown formatting)
158
+ const plainTextHeaders = [
159
+ { pattern: /^when\s+to\s+use$/i, section: "when_to_use" },
160
+ { pattern: /^when\s+not\s+to\s+use$/i, section: "when_not_to_use" },
161
+ { pattern: /^when\s+to\s+not\s+use$/i, section: "when_not_to_use" },
162
+ { pattern: /^don'?t\s+use$/i, section: "when_not_to_use" },
163
+ { pattern: /^do\s+not\s+use$/i, section: "when_not_to_use" },
164
+ { pattern: /^accessibility$/i, section: "accessibility" },
165
+ { pattern: /^a11y$/i, section: "accessibility" },
166
+ { pattern: /^content\s*(requirements|guidelines)?$/i, section: "content", getHeading: true },
167
+ { pattern: /^writing\s*(guidelines)?$/i, section: "content", getHeading: true },
168
+ { pattern: /^copy\s*(guidelines)?$/i, section: "content", getHeading: true },
169
+ { pattern: /^variants$/i, section: "other" },
170
+ ];
171
+ // Detect Figma per-property documentation headers like:
172
+ // "Show Left Icon: True – Purpose", "Badge Text – Purpose", "Nested Instance: Checkbox – Purpose"
173
+ const propertyDocPattern = /[–-]\s*Purpose\s*$/i;
174
+ for (const line of lines) {
175
+ const trimmed = line.trim();
176
+ // Detect section headers: bold text (**Header**), markdown headers (## Header), or plain text exact matches
177
+ const markdownHeaderMatch = trimmed.match(/^(?:\*\*|#{1,6}\s*)(.+?)(?:\*\*)?$/);
178
+ const headerText = markdownHeaderMatch ? markdownHeaderMatch[1].trim().replace(/\*\*/g, "") : null;
179
+ // Check if this is a Figma per-property documentation block (e.g., "Show Left Icon: True – Purpose")
180
+ // These should be routed to "other" to avoid polluting content guidelines and accessibility sections
181
+ const rawTextForPropertyCheck = headerText || trimmed;
182
+ if (propertyDocPattern.test(rawTextForPropertyCheck)) {
183
+ currentSection = "other";
184
+ continue;
185
+ }
186
+ // Check plain-text headers first (exact line matches for known patterns)
187
+ let plainMatch = null;
188
+ if (!headerText) {
189
+ for (const ph of plainTextHeaders) {
190
+ if (ph.pattern.test(trimmed)) {
191
+ plainMatch = { section: ph.section, heading: ph.getHeading ? trimmed : "" };
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ // Resolve the effective header
197
+ const effectiveHeader = headerText || plainMatch?.heading || null;
198
+ const isHeader = headerText !== null || plainMatch !== null;
199
+ if (isHeader) {
200
+ // If we matched a plain-text header with a known section, use it directly
201
+ if (plainMatch) {
202
+ if (plainMatch.section === "content" && plainMatch.heading) {
203
+ currentContentHeading = plainMatch.heading;
204
+ result.contentGuidelines.push({ heading: plainMatch.heading, items: [] });
205
+ }
206
+ currentSection = plainMatch.section;
207
+ continue;
208
+ }
209
+ // Otherwise process the markdown header text
210
+ const lower = (effectiveHeader || "").toLowerCase();
211
+ if (lower.includes("when to use") && !lower.includes("not")) {
212
+ currentSection = "when_to_use";
213
+ continue;
214
+ }
215
+ else if (lower.includes("when not to use") || lower.includes("when to not use") || lower.includes("don't use") || lower.includes("do not use")) {
216
+ currentSection = "when_not_to_use";
217
+ continue;
218
+ }
219
+ else if (lower.includes("accessibility") || lower.includes("a11y") || lower.includes("aria")) {
220
+ currentSection = "accessibility";
221
+ continue;
222
+ }
223
+ else if (lower.includes("content") || lower.includes("title text") || lower.includes("description text") || lower.includes("button label") || lower.includes("writing") || lower.includes("copy")) {
224
+ currentSection = "content";
225
+ currentContentHeading = effectiveHeader || "";
226
+ result.contentGuidelines.push({ heading: effectiveHeader || "", items: [] });
227
+ continue;
228
+ }
229
+ else if (currentSection === "overview") {
230
+ // A header after overview text means we're moving to a new section
231
+ currentSection = "other";
232
+ // Check if this might be a content guideline sub-section
233
+ if (lower.includes("title") || lower.includes("description") || lower.includes("label") || lower.includes("variant")) {
234
+ currentSection = "content";
235
+ currentContentHeading = effectiveHeader || "";
236
+ result.contentGuidelines.push({ heading: effectiveHeader || "", items: [] });
237
+ continue;
238
+ }
239
+ }
240
+ else if (currentSection === "content") {
241
+ // New sub-heading within content guidelines
242
+ currentContentHeading = effectiveHeader || "";
243
+ result.contentGuidelines.push({ heading: effectiveHeader || "", items: [] });
244
+ continue;
245
+ }
246
+ }
247
+ // Skip empty lines (but don't change section)
248
+ if (!trimmed)
249
+ continue;
250
+ // Skip horizontal rules
251
+ if (/^---+$/.test(trimmed))
252
+ continue;
253
+ // Extract bullet content
254
+ const bulletMatch = trimmed.match(/^[-*•]\s*(.+)/);
255
+ const content = bulletMatch ? bulletMatch[1] : trimmed;
256
+ switch (currentSection) {
257
+ case "overview":
258
+ overviewLines.push(content);
259
+ break;
260
+ case "when_to_use":
261
+ result.whenToUse.push(content);
262
+ break;
263
+ case "when_not_to_use":
264
+ result.whenNotToUse.push(content);
265
+ break;
266
+ case "content": {
267
+ const last = result.contentGuidelines[result.contentGuidelines.length - 1];
268
+ if (last)
269
+ last.items.push(content);
270
+ break;
271
+ }
272
+ case "accessibility":
273
+ result.accessibilityNotes.push(content);
274
+ break;
275
+ case "other":
276
+ result.additionalNotes.push(content);
277
+ break;
278
+ }
279
+ }
280
+ result.overview = overviewLines.join(" ").trim();
281
+ return result;
282
+ }
283
+ /**
284
+ * Collect color data from all variants in a COMPONENT_SET.
285
+ * For single COMPONENTs, returns data for just that component.
286
+ */
287
+ export function collectAllVariantData(node, varNameMap) {
288
+ const variants = [];
289
+ const nodesToWalk = node.type === "COMPONENT_SET" && node.children?.length > 0
290
+ ? node.children
291
+ : [node];
292
+ for (const variant of nodesToWalk) {
293
+ const data = {
294
+ variantName: variant.name || "Default",
295
+ fills: [],
296
+ strokes: [],
297
+ textColors: [],
298
+ icons: [],
299
+ };
300
+ walkVariantNode(variant, data, varNameMap, 0, 5);
301
+ variants.push(data);
302
+ }
303
+ return variants;
304
+ }
305
+ /** Walk a single variant node tree to collect colors and icons */
306
+ function walkVariantNode(node, data, varNameMap, depth, maxDepth) {
307
+ if (depth > maxDepth)
308
+ return;
309
+ const isText = node.type === "TEXT";
310
+ // Check if this is an icon instance
311
+ if (node.type === "INSTANCE" && (node.name?.toLowerCase().includes("icon") ||
312
+ node.name?.toLowerCase().startsWith("icon"))) {
313
+ const iconName = node.name.replace(/^icon\s*\/?\s*/i, "").trim();
314
+ data.icons.push({ name: iconName || node.name, type: "instance" });
315
+ }
316
+ // Collect fills
317
+ if (node.fills && Array.isArray(node.fills)) {
318
+ for (const fill of node.fills) {
319
+ if (fill.type === "SOLID" && fill.color && fill.visible !== false) {
320
+ const hex = figmaRGBAToHex({ ...fill.color, a: fill.opacity ?? fill.color.a ?? 1 });
321
+ const varId = fill.boundVariables?.color?.id;
322
+ const entry = {
323
+ hex,
324
+ nodeName: node.name || "",
325
+ variableId: varId,
326
+ variableName: varId ? varNameMap.get(varId) : undefined,
327
+ };
328
+ if (isText) {
329
+ data.textColors.push(entry);
330
+ }
331
+ else {
332
+ data.fills.push(entry);
333
+ }
334
+ }
335
+ }
336
+ }
337
+ // Collect strokes
338
+ if (node.strokes && Array.isArray(node.strokes)) {
339
+ for (const stroke of node.strokes) {
340
+ if (stroke.type === "SOLID" && stroke.color && stroke.visible !== false) {
341
+ const hex = figmaRGBAToHex({ ...stroke.color, a: stroke.opacity ?? stroke.color.a ?? 1 });
342
+ const varId = stroke.boundVariables?.color?.id;
343
+ data.strokes.push({
344
+ hex,
345
+ nodeName: node.name || "",
346
+ variableId: varId,
347
+ variableName: varId ? varNameMap.get(varId) : undefined,
348
+ });
349
+ }
350
+ }
351
+ }
352
+ // Recurse into children
353
+ if (node.children && Array.isArray(node.children)) {
354
+ for (const child of node.children) {
355
+ walkVariantNode(child, data, varNameMap, depth + 1, maxDepth);
356
+ }
357
+ }
358
+ }
359
+ /**
360
+ * Collect typography data from all text nodes in a component tree.
361
+ */
362
+ export function collectTypographyData(node, depth = 0, maxDepth = 5) {
363
+ const results = [];
364
+ if (depth > maxDepth)
365
+ return results;
366
+ // For COMPONENT_SET, walk the default (first) variant
367
+ if (node.type === "COMPONENT_SET" && node.children?.length > 0 && depth === 0) {
368
+ return collectTypographyData(node.children[0], 0, maxDepth);
369
+ }
370
+ if (node.type === "TEXT" && node.style) {
371
+ const s = node.style;
372
+ const weightNames = {
373
+ 100: "Thin", 200: "ExtraLight", 300: "Light", 400: "Regular",
374
+ 500: "Medium", 600: "SemiBold", 700: "Bold", 800: "ExtraBold", 900: "Black",
375
+ };
376
+ results.push({
377
+ nodeName: node.name || "Text",
378
+ fontFamily: s.fontFamily || "Unknown",
379
+ fontWeight: s.fontWeight || 400,
380
+ fontWeightName: weightNames[s.fontWeight] || String(s.fontWeight),
381
+ fontSize: s.fontSize || 14,
382
+ lineHeight: s.lineHeightPx || s.fontSize || 14,
383
+ letterSpacing: s.letterSpacing || 0,
384
+ });
385
+ }
386
+ if (node.children && Array.isArray(node.children)) {
387
+ for (const child of node.children) {
388
+ results.push(...collectTypographyData(child, depth + 1, maxDepth));
389
+ }
390
+ }
391
+ return results;
392
+ }
393
+ /**
394
+ * Build an anatomy tree representation from a Figma node structure.
395
+ * Returns a formatted string showing the component's nested structure.
396
+ */
397
+ export function buildAnatomyTree(node, depth = 0, maxDepth = 5) {
398
+ if (depth > maxDepth)
399
+ return "";
400
+ // For COMPONENT_SET, pick the variant with the deepest children tree for the richest anatomy
401
+ let targetNode = node;
402
+ if (node.type === "COMPONENT_SET" && node.children?.length > 0 && depth === 0) {
403
+ let bestChild = node.children[0];
404
+ let bestDepth = countChildDepth(bestChild);
405
+ for (let i = 1; i < node.children.length; i++) {
406
+ const d = countChildDepth(node.children[i]);
407
+ if (d > bestDepth) {
408
+ bestDepth = d;
409
+ bestChild = node.children[i];
410
+ }
411
+ }
412
+ targetNode = bestChild;
413
+ }
414
+ const lines = [];
415
+ buildAnatomyLines(targetNode, lines, "", true, 0, maxDepth);
416
+ return lines.join("\n");
417
+ }
418
+ /** Count the maximum depth of a node's children tree */
419
+ function countChildDepth(node) {
420
+ if (!node.children || !Array.isArray(node.children) || node.children.length === 0)
421
+ return 0;
422
+ let max = 0;
423
+ for (const child of node.children) {
424
+ const d = countChildDepth(child);
425
+ if (d > max)
426
+ max = d;
427
+ }
428
+ return 1 + max;
429
+ }
430
+ function buildAnatomyLines(node, lines, prefix, isLast, depth, maxDepth) {
431
+ if (depth > maxDepth)
432
+ return;
433
+ const connector = depth === 0 ? "" : (isLast ? "└── " : "├── ");
434
+ const childPrefix = depth === 0 ? "" : (isLast ? " " : "│ ");
435
+ // Build node label
436
+ let label = node.name || node.type;
437
+ const typeHint = node.type === "TEXT" ? " (TEXT)"
438
+ : node.type === "INSTANCE" ? " (INSTANCE)"
439
+ : node.type === "COMPONENT" ? " (COMPONENT)"
440
+ : node.type === "FRAME" ? ""
441
+ : node.type === "VECTOR" ? " (VECTOR)"
442
+ : node.type === "RECTANGLE" ? " (RECTANGLE)"
443
+ : "";
444
+ // Add layout info for frames
445
+ let layoutInfo = "";
446
+ if (node.layoutMode) {
447
+ const dir = node.layoutMode === "HORIZONTAL" ? "horizontal" : "vertical";
448
+ layoutInfo = ` — ${dir} auto-layout`;
449
+ if (node.itemSpacing !== undefined)
450
+ layoutInfo += `, gap: ${node.itemSpacing}px`;
451
+ }
452
+ // Add sizing info
453
+ let sizingInfo = "";
454
+ if (node.primaryAxisSizingMode || node.counterAxisSizingMode) {
455
+ const parts = [];
456
+ if (node.primaryAxisSizingMode === "FIXED")
457
+ parts.push("fixed-width");
458
+ if (node.primaryAxisSizingMode === "AUTO")
459
+ parts.push("hug-content");
460
+ if (node.counterAxisSizingMode === "FIXED")
461
+ parts.push("fixed-height");
462
+ if (node.layoutGrow === 1)
463
+ parts.push("fill");
464
+ if (parts.length > 0)
465
+ sizingInfo = ` [${parts.join(", ")}]`;
466
+ }
467
+ lines.push(`${prefix}${connector}${label}${typeHint}${layoutInfo}${sizingInfo}`);
468
+ // Recurse into children
469
+ if (node.children && Array.isArray(node.children)) {
470
+ const visibleChildren = node.children.filter((c) => c.visible !== false);
471
+ for (let i = 0; i < visibleChildren.length; i++) {
472
+ const isChildLast = i === visibleChildren.length - 1;
473
+ buildAnatomyLines(visibleChildren[i], lines, prefix + childPrefix, isChildLast, depth + 1, maxDepth);
474
+ }
475
+ }
476
+ }
477
+ /**
478
+ * Collect spacing tokens with their bound variable names.
479
+ */
480
+ function collectSpacingTokens(node) {
481
+ const tokens = [];
482
+ const boundVars = node.boundVariables || {};
483
+ const spacingProps = [
484
+ { key: "paddingTop", label: "Padding top" },
485
+ { key: "paddingRight", label: "Padding right" },
486
+ { key: "paddingBottom", label: "Padding bottom" },
487
+ { key: "paddingLeft", label: "Padding left" },
488
+ { key: "itemSpacing", label: "Gap" },
489
+ { key: "cornerRadius", label: "Border radius" },
490
+ { key: "strokeWeight", label: "Border width" },
491
+ ];
492
+ for (const { key, label } of spacingProps) {
493
+ const value = node[key];
494
+ if (value !== undefined && value !== null) {
495
+ const varBinding = boundVars[key];
496
+ const varName = varBinding?.id || varBinding?.name;
497
+ tokens.push({
498
+ property: label,
499
+ value,
500
+ variableName: typeof varName === "string" ? varName : undefined,
501
+ });
502
+ }
503
+ }
504
+ return tokens;
505
+ }
506
+ // ============================================================================
507
+ // Component Set Resolution Helpers
508
+ // ============================================================================
509
+ /**
510
+ * Resolve the node to use for visual/spacing/typography comparisons.
511
+ * COMPONENT_SET frames have container-level styling (Figma's purple dashed stroke,
512
+ * default cornerRadius: 5, organizational padding) that are NOT actual design specs.
513
+ * The real design properties live on the child COMPONENT variants.
514
+ * Returns the default variant (first child) for COMPONENT_SET, or the node itself otherwise.
515
+ */
516
+ export function resolveVisualNode(node) {
517
+ if (node.type === "COMPONENT_SET" && node.children?.length > 0) {
518
+ return node.children[0];
519
+ }
520
+ return node;
521
+ }
522
+ /** Detect if a node name is a Figma variant pattern like "Variant=Default, State=Hover, Size=lg" */
523
+ export function isVariantName(name) {
524
+ return /^[A-Za-z]+=.+,.+[A-Za-z]+=/.test(name);
525
+ }
526
+ /** Sanitize a component name for use as a file path */
527
+ export function sanitizeComponentName(name) {
528
+ return name.replace(/[^a-zA-Z0-9-_ ]/g, "").replace(/\s+/g, "-").trim();
529
+ }
530
+ /**
531
+ * Resolve the parent COMPONENT_SET info for a variant COMPONENT node.
532
+ * Returns the set's name, nodeId, and componentPropertyDefinitions.
533
+ */
534
+ async function resolveComponentSetInfo(api, fileKey, nodeId, componentMeta, allComponentsMeta) {
535
+ const empty = { setName: null, setNodeId: null, propertyDefinitions: {} };
536
+ // Strategy 1: Check componentMeta for component_set_id (from /files/:key/components)
537
+ const meta = componentMeta || allComponentsMeta?.find((c) => c.node_id === nodeId);
538
+ const setId = meta?.component_set_id;
539
+ const setName = meta?.component_set_name || null;
540
+ if (!setId) {
541
+ // Strategy 2: Use containing_frame from component metadata
542
+ const containingFrame = meta?.containing_frame;
543
+ if (containingFrame?.containingComponentSet && containingFrame?.nodeId) {
544
+ const frameNodeId = containingFrame.nodeId;
545
+ try {
546
+ const setResponse = await api.getNodes(fileKey, [frameNodeId], { depth: 1 });
547
+ const setNode = setResponse?.nodes?.[frameNodeId]?.document;
548
+ if (setNode?.componentPropertyDefinitions) {
549
+ return {
550
+ setName: setNode.name || containingFrame.name || setName,
551
+ setNodeId: frameNodeId,
552
+ propertyDefinitions: setNode.componentPropertyDefinitions,
553
+ };
554
+ }
555
+ }
556
+ catch {
557
+ logger.warn("Could not fetch component set via containing_frame");
558
+ }
559
+ }
560
+ // Strategy 3: Search getComponentSets for a set matching this component's containing frame
561
+ try {
562
+ const setsResponse = await api.getComponentSets(fileKey);
563
+ const sets = setsResponse?.meta?.component_sets;
564
+ if (sets && Array.isArray(sets)) {
565
+ // Match by containing_frame name or by node name prefix
566
+ const nodeName = allComponentsMeta?.find((c) => c.node_id === nodeId)?.name || "";
567
+ const matchingSet = containingFrame?.name
568
+ ? sets.find((s) => s.name === containingFrame.name)
569
+ : null;
570
+ if (matchingSet) {
571
+ const setNodeId = matchingSet.node_id;
572
+ const setNodeResponse = await api.getNodes(fileKey, [setNodeId], { depth: 1 });
573
+ const setNode = setNodeResponse?.nodes?.[setNodeId]?.document;
574
+ if (setNode?.componentPropertyDefinitions) {
575
+ return {
576
+ setName: setNode.name || matchingSet.name,
577
+ setNodeId,
578
+ propertyDefinitions: setNode.componentPropertyDefinitions,
579
+ };
580
+ }
581
+ }
582
+ }
583
+ }
584
+ catch {
585
+ logger.warn("Could not resolve component set via getComponentSets");
586
+ }
587
+ return { ...empty, setName };
588
+ }
589
+ // Fetch the COMPONENT_SET node to get componentPropertyDefinitions
590
+ try {
591
+ const setResponse = await api.getNodes(fileKey, [setId], { depth: 1 });
592
+ const setNode = setResponse?.nodes?.[setId]?.document;
593
+ if (setNode) {
594
+ return {
595
+ setName: setNode.name || setName,
596
+ setNodeId: setId,
597
+ propertyDefinitions: setNode.componentPropertyDefinitions || {},
598
+ };
599
+ }
600
+ }
601
+ catch {
602
+ logger.warn({ setId }, "Could not fetch component set node");
603
+ }
604
+ return { ...empty, setName };
605
+ }
606
+ /**
607
+ * Extract a clean component name from a node, resolving variant patterns.
608
+ * "Variant=Default, State=Default, Size=default" -> uses parent set name or codeSpec fallback.
609
+ * Preserves the casing of whatever source provides the name (no assumptions about convention).
610
+ */
611
+ function resolveComponentName(node, setName, fallbackName) {
612
+ // If we have a parent set name, prefer it (authoritative from Figma)
613
+ if (setName)
614
+ return setName;
615
+ // If the node name is a variant pattern, try to extract something useful
616
+ if (isVariantName(node.name)) {
617
+ // Use fallback (from codeSpec.metadata.name or file path) preserving original casing
618
+ if (fallbackName)
619
+ return fallbackName;
620
+ // Last resort: extract first variant value as name hint
621
+ return node.name.split(",")[0].split("=")[1]?.trim() || node.name;
622
+ }
623
+ return node.name || fallbackName || "Component";
624
+ }
625
+ // ============================================================================
626
+ // Parity Comparators
627
+ // ============================================================================
628
+ function compareVisual(node, codeSpec, discrepancies) {
629
+ const cv = codeSpec.visual;
630
+ if (!cv)
631
+ return;
632
+ // Background / fill color
633
+ const figmaFill = extractFirstFillColor(node.fills);
634
+ if (figmaFill && cv.backgroundColor) {
635
+ const normalizedDesign = normalizeColor(figmaFill);
636
+ const normalizedCode = normalizeColor(cv.backgroundColor);
637
+ if (normalizedDesign !== normalizedCode) {
638
+ discrepancies.push({
639
+ category: "visual",
640
+ property: "backgroundColor",
641
+ severity: "major",
642
+ designValue: figmaFill,
643
+ codeValue: cv.backgroundColor,
644
+ message: `Background color mismatch: design=${figmaFill}, code=${cv.backgroundColor}`,
645
+ suggestion: `Update to match ${figmaFill}`,
646
+ });
647
+ }
648
+ }
649
+ // Border / stroke color
650
+ const figmaStroke = extractFirstStrokeColor(node.strokes);
651
+ if (figmaStroke && cv.borderColor) {
652
+ const normalizedDesign = normalizeColor(figmaStroke);
653
+ const normalizedCode = normalizeColor(cv.borderColor);
654
+ if (normalizedDesign !== normalizedCode) {
655
+ discrepancies.push({
656
+ category: "visual",
657
+ property: "borderColor",
658
+ severity: "major",
659
+ designValue: figmaStroke,
660
+ codeValue: cv.borderColor,
661
+ message: `Border color mismatch: design=${figmaStroke}, code=${cv.borderColor}`,
662
+ suggestion: `Update to match ${figmaStroke}`,
663
+ });
664
+ }
665
+ }
666
+ // Border width / stroke weight
667
+ if (node.strokeWeight !== undefined && cv.borderWidth !== undefined) {
668
+ if (!numericClose(node.strokeWeight, cv.borderWidth)) {
669
+ discrepancies.push({
670
+ category: "visual",
671
+ property: "borderWidth",
672
+ severity: "minor",
673
+ designValue: node.strokeWeight,
674
+ codeValue: cv.borderWidth,
675
+ message: `Border width mismatch: design=${node.strokeWeight}px, code=${cv.borderWidth}px`,
676
+ });
677
+ }
678
+ }
679
+ // Corner radius
680
+ const figmaRadius = node.cornerRadius;
681
+ if (figmaRadius !== undefined && cv.borderRadius !== undefined) {
682
+ const codeRadius = typeof cv.borderRadius === "string" ? parseFloat(cv.borderRadius) : cv.borderRadius;
683
+ if (!isNaN(codeRadius) && !numericClose(figmaRadius, codeRadius)) {
684
+ discrepancies.push({
685
+ category: "visual",
686
+ property: "borderRadius",
687
+ severity: "minor",
688
+ designValue: figmaRadius,
689
+ codeValue: cv.borderRadius,
690
+ message: `Border radius mismatch: design=${figmaRadius}px, code=${cv.borderRadius}`,
691
+ });
692
+ }
693
+ }
694
+ // Opacity
695
+ if (node.opacity !== undefined && cv.opacity !== undefined) {
696
+ if (!numericClose(node.opacity, cv.opacity, 0.01)) {
697
+ discrepancies.push({
698
+ category: "visual",
699
+ property: "opacity",
700
+ severity: "minor",
701
+ designValue: node.opacity,
702
+ codeValue: cv.opacity,
703
+ message: `Opacity mismatch: design=${node.opacity}, code=${cv.opacity}`,
704
+ });
705
+ }
706
+ }
707
+ }
708
+ function compareSpacing(node, codeSpec, discrepancies) {
709
+ const cs = codeSpec.spacing;
710
+ if (!cs)
711
+ return;
712
+ const designSpacing = extractSpacingProperties(node);
713
+ const spacingProps = [
714
+ { key: "paddingTop", designKey: "paddingTop" },
715
+ { key: "paddingRight", designKey: "paddingRight" },
716
+ { key: "paddingBottom", designKey: "paddingBottom" },
717
+ { key: "paddingLeft", designKey: "paddingLeft" },
718
+ { key: "gap", designKey: "gap" },
719
+ ];
720
+ for (const { key, designKey } of spacingProps) {
721
+ const dVal = designSpacing[designKey];
722
+ const cVal = cs[key];
723
+ if (dVal !== undefined && cVal !== undefined) {
724
+ const cNum = typeof cVal === "string" ? parseFloat(cVal) : cVal;
725
+ if (!isNaN(cNum) && !numericClose(dVal, cNum)) {
726
+ discrepancies.push({
727
+ category: "spacing",
728
+ property: key,
729
+ severity: "major",
730
+ designValue: dVal,
731
+ codeValue: cVal,
732
+ message: `Spacing mismatch on ${key}: design=${dVal}px, code=${cVal}`,
733
+ });
734
+ }
735
+ }
736
+ }
737
+ // Width/height (only compare if both are numeric)
738
+ if (designSpacing.width !== undefined && cs.width !== undefined) {
739
+ const cNum = typeof cs.width === "string" ? parseFloat(cs.width) : cs.width;
740
+ if (!isNaN(cNum) && !numericClose(designSpacing.width, cNum, 2)) {
741
+ discrepancies.push({
742
+ category: "spacing",
743
+ property: "width",
744
+ severity: "minor",
745
+ designValue: designSpacing.width,
746
+ codeValue: cs.width,
747
+ message: `Width mismatch: design=${designSpacing.width}px, code=${cs.width}`,
748
+ });
749
+ }
750
+ }
751
+ if (designSpacing.height !== undefined && cs.height !== undefined) {
752
+ const cNum = typeof cs.height === "string" ? parseFloat(cs.height) : cs.height;
753
+ if (!isNaN(cNum) && !numericClose(designSpacing.height, cNum, 2)) {
754
+ discrepancies.push({
755
+ category: "spacing",
756
+ property: "height",
757
+ severity: "minor",
758
+ designValue: designSpacing.height,
759
+ codeValue: cs.height,
760
+ message: `Height mismatch: design=${designSpacing.height}px, code=${cs.height}`,
761
+ });
762
+ }
763
+ }
764
+ }
765
+ function compareTypography(node, codeSpec, discrepancies) {
766
+ const ct = codeSpec.typography;
767
+ if (!ct)
768
+ return;
769
+ // Find text nodes in children or check the node itself
770
+ let textNode = node.type === "TEXT" ? node : null;
771
+ if (!textNode && node.children) {
772
+ textNode = node.children.find((c) => c.type === "TEXT");
773
+ }
774
+ if (!textNode)
775
+ return;
776
+ const designTypo = extractTextProperties(textNode);
777
+ if (designTypo.fontFamily && ct.fontFamily) {
778
+ if (designTypo.fontFamily.toLowerCase() !== ct.fontFamily.toLowerCase()) {
779
+ discrepancies.push({
780
+ category: "typography",
781
+ property: "fontFamily",
782
+ severity: "major",
783
+ designValue: designTypo.fontFamily,
784
+ codeValue: ct.fontFamily,
785
+ message: `Font family mismatch: design="${designTypo.fontFamily}", code="${ct.fontFamily}"`,
786
+ });
787
+ }
788
+ }
789
+ if (designTypo.fontSize && ct.fontSize) {
790
+ if (!numericClose(designTypo.fontSize, ct.fontSize)) {
791
+ discrepancies.push({
792
+ category: "typography",
793
+ property: "fontSize",
794
+ severity: "major",
795
+ designValue: designTypo.fontSize,
796
+ codeValue: ct.fontSize,
797
+ message: `Font size mismatch: design=${designTypo.fontSize}px, code=${ct.fontSize}px`,
798
+ });
799
+ }
800
+ }
801
+ if (designTypo.fontWeight && ct.fontWeight) {
802
+ const codeWeight = typeof ct.fontWeight === "string" ? parseInt(ct.fontWeight, 10) : ct.fontWeight;
803
+ if (!isNaN(codeWeight) && designTypo.fontWeight !== codeWeight) {
804
+ discrepancies.push({
805
+ category: "typography",
806
+ property: "fontWeight",
807
+ severity: "minor",
808
+ designValue: designTypo.fontWeight,
809
+ codeValue: ct.fontWeight,
810
+ message: `Font weight mismatch: design=${designTypo.fontWeight}, code=${ct.fontWeight}`,
811
+ });
812
+ }
813
+ }
814
+ if (designTypo.lineHeight && ct.lineHeight) {
815
+ const codeLH = typeof ct.lineHeight === "string" ? parseFloat(ct.lineHeight) : ct.lineHeight;
816
+ if (!isNaN(codeLH) && !numericClose(designTypo.lineHeight, codeLH, 1)) {
817
+ discrepancies.push({
818
+ category: "typography",
819
+ property: "lineHeight",
820
+ severity: "minor",
821
+ designValue: designTypo.lineHeight,
822
+ codeValue: ct.lineHeight,
823
+ message: `Line height mismatch: design=${designTypo.lineHeight}px, code=${ct.lineHeight}`,
824
+ });
825
+ }
826
+ }
827
+ }
828
+ function compareTokens(enrichedData, codeSpec, discrepancies) {
829
+ const ct = codeSpec.tokens;
830
+ if (!ct || !enrichedData)
831
+ return;
832
+ // Check for hardcoded values in design
833
+ if (enrichedData.hardcoded_values && enrichedData.hardcoded_values.length > 0) {
834
+ for (const hv of enrichedData.hardcoded_values) {
835
+ discrepancies.push({
836
+ category: "tokens",
837
+ property: `hardcoded:${hv.property}`,
838
+ severity: "major",
839
+ designValue: `${hv.value} (hardcoded)`,
840
+ codeValue: null,
841
+ message: `Design has hardcoded ${hv.type} value "${hv.value}" on ${hv.property}. Should use token${hv.suggested_token ? `: ${hv.suggested_token}` : ""}`,
842
+ suggestion: hv.suggested_token ? `Use token: ${hv.suggested_token}` : undefined,
843
+ });
844
+ }
845
+ }
846
+ // Cross-reference design variables with code tokens
847
+ if (enrichedData.variables_used && ct.usedTokens) {
848
+ const designTokenNames = enrichedData.variables_used.map((v) => v.name.toLowerCase());
849
+ const codeTokenNames = ct.usedTokens.map((t) => t.toLowerCase());
850
+ for (const designToken of enrichedData.variables_used) {
851
+ const normalizedName = designToken.name.toLowerCase();
852
+ if (!codeTokenNames.some((ct) => ct.includes(normalizedName) || normalizedName.includes(ct))) {
853
+ discrepancies.push({
854
+ category: "tokens",
855
+ property: `token:${designToken.name}`,
856
+ severity: "minor",
857
+ designValue: designToken.name,
858
+ codeValue: null,
859
+ message: `Design uses token "${designToken.name}" but code doesn't reference it`,
860
+ suggestion: `Add token reference in code`,
861
+ });
862
+ }
863
+ }
864
+ for (const codeToken of ct.usedTokens) {
865
+ const normalizedName = codeToken.toLowerCase();
866
+ if (!designTokenNames.some((dt) => dt.includes(normalizedName) || normalizedName.includes(dt))) {
867
+ discrepancies.push({
868
+ category: "tokens",
869
+ property: `token:${codeToken}`,
870
+ severity: "info",
871
+ designValue: null,
872
+ codeValue: codeToken,
873
+ message: `Code uses token "${codeToken}" but design doesn't reference it`,
874
+ });
875
+ }
876
+ }
877
+ }
878
+ // Token coverage
879
+ if (enrichedData.token_coverage !== undefined && enrichedData.token_coverage < 80) {
880
+ discrepancies.push({
881
+ category: "tokens",
882
+ property: "tokenCoverage",
883
+ severity: enrichedData.token_coverage < 50 ? "critical" : "major",
884
+ designValue: `${enrichedData.token_coverage}%`,
885
+ codeValue: null,
886
+ message: `Design token coverage is ${enrichedData.token_coverage}% (target: ≥80%)`,
887
+ suggestion: "Replace hardcoded values with design tokens",
888
+ });
889
+ }
890
+ }
891
+ function compareComponentAPI(node, codeSpec, discrepancies) {
892
+ const ca = codeSpec.componentAPI;
893
+ if (!ca?.props)
894
+ return;
895
+ const figmaProps = node.componentPropertyDefinitions || {};
896
+ const figmaPropNames = Object.keys(figmaProps);
897
+ // Map Figma property types
898
+ const figmaPropList = figmaPropNames.map((name) => ({
899
+ name,
900
+ type: figmaProps[name].type,
901
+ defaultValue: figmaProps[name].defaultValue,
902
+ values: figmaProps[name].variantOptions || [],
903
+ }));
904
+ // Check each code prop against Figma properties
905
+ for (const codeProp of ca.props) {
906
+ const matchingFigma = figmaPropList.find((fp) => fp.name.toLowerCase().replace(/[^a-z0-9]/g, "") === codeProp.name.toLowerCase().replace(/[^a-z0-9]/g, ""));
907
+ if (!matchingFigma) {
908
+ discrepancies.push({
909
+ category: "componentAPI",
910
+ property: `prop:${codeProp.name}`,
911
+ severity: "minor",
912
+ designValue: null,
913
+ codeValue: codeProp.name,
914
+ message: `Code prop "${codeProp.name}" has no matching Figma component property`,
915
+ suggestion: `Add component property in Figma`,
916
+ });
917
+ }
918
+ else if (matchingFigma.type === "VARIANT" && codeProp.values) {
919
+ // Check variant values match
920
+ const figmaValues = matchingFigma.values.map((v) => v.toLowerCase());
921
+ const codeValues = codeProp.values.map((v) => v.toLowerCase());
922
+ const missingInDesign = codeValues.filter((v) => !figmaValues.includes(v));
923
+ const missingInCode = figmaValues.filter((v) => !codeValues.includes(v));
924
+ if (missingInDesign.length > 0) {
925
+ discrepancies.push({
926
+ category: "componentAPI",
927
+ property: `prop:${codeProp.name}:values`,
928
+ severity: "major",
929
+ designValue: matchingFigma.values.join(", "),
930
+ codeValue: codeProp.values.join(", "),
931
+ message: `Code has variant values not in design: ${missingInDesign.join(", ")}`,
932
+ });
933
+ }
934
+ if (missingInCode.length > 0) {
935
+ discrepancies.push({
936
+ category: "componentAPI",
937
+ property: `prop:${codeProp.name}:values`,
938
+ severity: "info",
939
+ designValue: matchingFigma.values.join(", "),
940
+ codeValue: codeProp.values.join(", "),
941
+ message: `Design has variant values not in code: ${missingInCode.join(", ")}`,
942
+ });
943
+ }
944
+ }
945
+ }
946
+ // Check for Figma properties not in code
947
+ for (const figmaProp of figmaPropList) {
948
+ const matchingCode = ca.props.find((cp) => cp.name.toLowerCase().replace(/[^a-z0-9]/g, "") === figmaProp.name.toLowerCase().replace(/[^a-z0-9]/g, ""));
949
+ if (!matchingCode) {
950
+ discrepancies.push({
951
+ category: "componentAPI",
952
+ property: `prop:${figmaProp.name}`,
953
+ severity: "info",
954
+ designValue: figmaProp.name,
955
+ codeValue: null,
956
+ message: `Figma property "${figmaProp.name}" (${figmaProp.type}) has no matching code prop`,
957
+ });
958
+ }
959
+ }
960
+ }
961
+ function compareAccessibility(node, codeSpec, discrepancies) {
962
+ const ca = codeSpec.accessibility;
963
+ if (!ca)
964
+ return;
965
+ // Check description/annotations for accessibility hints
966
+ const description = node.descriptionMarkdown || node.description || "";
967
+ const descLower = description.toLowerCase();
968
+ const hasAriaAnnotation = descLower.includes("aria") || descLower.includes("accessibility");
969
+ // ---- 1. ARIA Role Parity ----
970
+ if (ca.role && !hasAriaAnnotation) {
971
+ discrepancies.push({
972
+ category: "accessibility",
973
+ property: "role",
974
+ severity: "info",
975
+ designValue: null,
976
+ codeValue: ca.role,
977
+ message: `Code defines role="${ca.role}" but design has no accessibility annotations`,
978
+ suggestion: "Add accessibility annotations in Figma description",
979
+ });
980
+ }
981
+ // ---- 2. Semantic Element vs Component Name ----
982
+ if (ca.semanticElement) {
983
+ const nodeName = (node.name || "").toLowerCase();
984
+ const element = ca.semanticElement.toLowerCase();
985
+ // Check if interactive component uses correct semantic element
986
+ const interactivePattern = /button|link|input|checkbox|radio|switch|toggle|tab|select/i;
987
+ if (interactivePattern.test(nodeName)) {
988
+ const elementMatchesDesign = (nodeName.includes("button") && (element === "button" || ca.role === "button")) ||
989
+ (nodeName.includes("link") && (element === "a" || ca.role === "link")) ||
990
+ (nodeName.includes("input") && (element === "input" || element === "textarea")) ||
991
+ (nodeName.includes("checkbox") && (element === "input" || ca.role === "checkbox")) ||
992
+ (nodeName.includes("radio") && (element === "input" || ca.role === "radio")) ||
993
+ (nodeName.includes("switch") && (ca.role === "switch" || element === "input")) ||
994
+ (nodeName.includes("select") && (element === "select" || ca.role === "listbox")) ||
995
+ (nodeName.includes("tab") && (ca.role === "tab" || element === "button"));
996
+ if (!elementMatchesDesign) {
997
+ discrepancies.push({
998
+ category: "accessibility",
999
+ property: "semanticElement",
1000
+ severity: "major",
1001
+ designValue: nodeName,
1002
+ codeValue: `<${element}>${ca.role ? ` role="${ca.role}"` : ""}`,
1003
+ message: `Design component "${node.name}" may not match code element <${element}>`,
1004
+ suggestion: `Verify that <${element}> is the correct semantic element for a component named "${node.name}". Use native HTML elements over ARIA roles where possible.`,
1005
+ });
1006
+ }
1007
+ }
1008
+ }
1009
+ // ---- 3. Contrast Ratio ----
1010
+ if (ca.contrastRatio !== undefined && ca.contrastRatio < 4.5) {
1011
+ discrepancies.push({
1012
+ category: "accessibility",
1013
+ property: "contrastRatio",
1014
+ severity: "critical",
1015
+ designValue: null,
1016
+ codeValue: ca.contrastRatio,
1017
+ message: `Contrast ratio ${ca.contrastRatio}:1 fails WCAG AA minimum (4.5:1)`,
1018
+ suggestion: "Increase contrast ratio to at least 4.5:1",
1019
+ });
1020
+ }
1021
+ // ---- 4. Focus Indicator Parity ----
1022
+ // Check if design has a focus variant but code doesn't implement focus-visible
1023
+ const variants = node.children || [];
1024
+ const hasFocusVariant = variants.some((v) => /focus|focused/i.test(v.name || ""));
1025
+ if (hasFocusVariant && ca.focusVisible === false) {
1026
+ discrepancies.push({
1027
+ category: "accessibility",
1028
+ property: "focusVisible",
1029
+ severity: "critical",
1030
+ designValue: "focus variant exists",
1031
+ codeValue: "focusVisible: false",
1032
+ message: "Design has a focus variant but code does not implement :focus-visible styles",
1033
+ suggestion: "Add :focus-visible CSS with a visible focus ring matching the design's focus variant (WCAG 2.4.7)",
1034
+ });
1035
+ }
1036
+ else if (!hasFocusVariant && ca.focusVisible === true) {
1037
+ discrepancies.push({
1038
+ category: "accessibility",
1039
+ property: "focusVisible",
1040
+ severity: "minor",
1041
+ designValue: "no focus variant",
1042
+ codeValue: "focusVisible: true",
1043
+ message: "Code implements :focus-visible but design has no focus variant to specify the visual treatment",
1044
+ suggestion: "Add a focus/focused variant in Figma to document the intended focus indicator design",
1045
+ });
1046
+ }
1047
+ // ---- 5. Disabled State Parity ----
1048
+ const hasDisabledVariant = variants.some((v) => /disabled|inactive/i.test(v.name || ""));
1049
+ if (hasDisabledVariant && ca.supportsDisabled === false) {
1050
+ discrepancies.push({
1051
+ category: "accessibility",
1052
+ property: "disabled",
1053
+ severity: "major",
1054
+ designValue: "disabled variant exists",
1055
+ codeValue: "supportsDisabled: false",
1056
+ message: "Design has a disabled variant but code does not support disabled/aria-disabled state",
1057
+ suggestion: "Implement disabled or aria-disabled attribute support in the component",
1058
+ });
1059
+ }
1060
+ else if (!hasDisabledVariant && ca.supportsDisabled === true) {
1061
+ discrepancies.push({
1062
+ category: "accessibility",
1063
+ property: "disabled",
1064
+ severity: "minor",
1065
+ designValue: "no disabled variant",
1066
+ codeValue: "supportsDisabled: true",
1067
+ message: "Code supports disabled state but design has no disabled variant",
1068
+ suggestion: "Add a disabled variant in Figma showing the visual treatment for disabled state",
1069
+ });
1070
+ }
1071
+ // ---- 6. Error State Parity ----
1072
+ const hasErrorVariant = variants.some((v) => /error|invalid|danger/i.test(v.name || ""));
1073
+ if (hasErrorVariant && ca.supportsError === false) {
1074
+ discrepancies.push({
1075
+ category: "accessibility",
1076
+ property: "errorState",
1077
+ severity: "major",
1078
+ designValue: "error variant exists",
1079
+ codeValue: "supportsError: false",
1080
+ message: "Design has an error variant but code does not support aria-invalid or error messaging",
1081
+ suggestion: "Implement aria-invalid attribute and associated error message (aria-describedby) in the component",
1082
+ });
1083
+ }
1084
+ // ---- 7. Required Field Parity ----
1085
+ if (ca.ariaRequired !== undefined) {
1086
+ const hasRequiredVariant = variants.some((v) => /required/i.test(v.name || ""));
1087
+ const hasRequiredInDescription = descLower.includes("required");
1088
+ if (ca.ariaRequired && !hasRequiredVariant && !hasRequiredInDescription) {
1089
+ discrepancies.push({
1090
+ category: "accessibility",
1091
+ property: "required",
1092
+ severity: "minor",
1093
+ designValue: "no required indicator",
1094
+ codeValue: "ariaRequired: true",
1095
+ message: "Code marks field as required but design has no visual required indicator",
1096
+ suggestion: "Add a required indicator (asterisk, label text) in the design and/or a required variant",
1097
+ });
1098
+ }
1099
+ }
1100
+ // ---- 8. Target Size Parity ----
1101
+ if (ca.renderedSize) {
1102
+ const [codeWidth, codeHeight] = ca.renderedSize;
1103
+ const designWidth = node.absoluteBoundingBox?.width || node.size?.x;
1104
+ const designHeight = node.absoluteBoundingBox?.height || node.size?.y;
1105
+ if (designWidth && designHeight) {
1106
+ // Check if code size is significantly smaller than design (>20% reduction)
1107
+ if (codeWidth < designWidth * 0.8 || codeHeight < designHeight * 0.8) {
1108
+ discrepancies.push({
1109
+ category: "accessibility",
1110
+ property: "targetSize",
1111
+ severity: "major",
1112
+ designValue: `${Math.round(designWidth)}x${Math.round(designHeight)}`,
1113
+ codeValue: `${codeWidth}x${codeHeight}`,
1114
+ message: `Code renders significantly smaller (${codeWidth}x${codeHeight}px) than design (${Math.round(designWidth)}x${Math.round(designHeight)}px)`,
1115
+ suggestion: "Ensure rendered component meets the design's touch target size. Check CSS min-width/min-height.",
1116
+ });
1117
+ }
1118
+ // Check WCAG 2.5.8 minimum (24x24)
1119
+ if (codeWidth < 24 || codeHeight < 24) {
1120
+ discrepancies.push({
1121
+ category: "accessibility",
1122
+ property: "targetSize",
1123
+ severity: "critical",
1124
+ designValue: `${Math.round(designWidth)}x${Math.round(designHeight)}`,
1125
+ codeValue: `${codeWidth}x${codeHeight}`,
1126
+ message: `Code renders below WCAG 2.5.8 minimum (24x24px): ${codeWidth}x${codeHeight}px`,
1127
+ suggestion: "Increase touch target size to at least 24x24px",
1128
+ });
1129
+ }
1130
+ }
1131
+ }
1132
+ // ---- 9. Keyboard Interactions ----
1133
+ if (ca.keyboardInteractions && ca.keyboardInteractions.length > 0 && !descLower.includes("keyboard")) {
1134
+ discrepancies.push({
1135
+ category: "accessibility",
1136
+ property: "keyboardInteractions",
1137
+ severity: "info",
1138
+ designValue: null,
1139
+ codeValue: ca.keyboardInteractions.join(", "),
1140
+ message: `Code defines keyboard interactions (${ca.keyboardInteractions.join(", ")}) but design has no keyboard documentation`,
1141
+ suggestion: "Document keyboard interactions in the Figma component description for developer handoff",
1142
+ });
1143
+ }
1144
+ }
1145
+ function compareNaming(node, codeSpec, discrepancies) {
1146
+ const cm = codeSpec.metadata;
1147
+ if (!cm?.name)
1148
+ return;
1149
+ const designName = node.name || "";
1150
+ const codeName = cm.name;
1151
+ // Check PascalCase consistency
1152
+ const isPascal = (s) => /^[A-Z][a-zA-Z0-9]*$/.test(s);
1153
+ const isKebab = (s) => /^[a-z][a-z0-9-]*$/.test(s);
1154
+ if (isPascal(designName) !== isPascal(codeName) && isKebab(designName) !== isKebab(codeName)) {
1155
+ discrepancies.push({
1156
+ category: "naming",
1157
+ property: "componentName",
1158
+ severity: "info",
1159
+ designValue: designName,
1160
+ codeValue: codeName,
1161
+ message: `Naming convention differs: design="${designName}", code="${codeName}"`,
1162
+ suggestion: "Align naming conventions between design and code",
1163
+ });
1164
+ }
1165
+ }
1166
+ function compareMetadata(node, componentMeta, codeSpec, discrepancies) {
1167
+ const cm = codeSpec.metadata;
1168
+ if (!cm)
1169
+ return;
1170
+ const designDesc = node.description || componentMeta?.description || "";
1171
+ if (cm.description && designDesc) {
1172
+ // Only flag if descriptions are meaningfully different (not just formatting)
1173
+ const normalizeDesc = (d) => d.toLowerCase().replace(/[^a-z0-9 ]/g, "").trim();
1174
+ if (normalizeDesc(designDesc) !== normalizeDesc(cm.description)) {
1175
+ discrepancies.push({
1176
+ category: "metadata",
1177
+ property: "description",
1178
+ severity: "info",
1179
+ designValue: designDesc.slice(0, 100),
1180
+ codeValue: cm.description.slice(0, 100),
1181
+ message: "Component descriptions differ between design and code",
1182
+ });
1183
+ }
1184
+ }
1185
+ if (cm.status && componentMeta?.description) {
1186
+ // Check if design description contains a status
1187
+ const statusKeywords = ["stable", "experimental", "deprecated", "beta", "alpha", "draft"];
1188
+ const designStatus = statusKeywords.find((s) => componentMeta.description.toLowerCase().includes(s));
1189
+ if (designStatus && designStatus !== cm.status.toLowerCase()) {
1190
+ discrepancies.push({
1191
+ category: "metadata",
1192
+ property: "status",
1193
+ severity: "minor",
1194
+ designValue: designStatus,
1195
+ codeValue: cm.status,
1196
+ message: `Status mismatch: design implies "${designStatus}", code says "${cm.status}"`,
1197
+ });
1198
+ }
1199
+ }
1200
+ }
1201
+ // ============================================================================
1202
+ // Action Item Generator
1203
+ // ============================================================================
1204
+ function generateActionItems(discrepancies, nodeId, canonicalSource, filePath) {
1205
+ const items = [];
1206
+ for (let i = 0; i < discrepancies.length; i++) {
1207
+ const d = discrepancies[i];
1208
+ // Determine which side needs the fix
1209
+ const fixSide = canonicalSource === "design" ? "code" : "design";
1210
+ const item = {
1211
+ discrepancyIndex: i,
1212
+ side: fixSide,
1213
+ };
1214
+ if (fixSide === "design") {
1215
+ // Generate Figma tool call parameters
1216
+ switch (d.category) {
1217
+ case "visual":
1218
+ if (d.property === "backgroundColor" && d.codeValue) {
1219
+ item.figmaTool = "figma_set_fills";
1220
+ item.figmaToolParams = {
1221
+ nodeId,
1222
+ fills: [{ type: "SOLID", color: String(d.codeValue) }],
1223
+ };
1224
+ }
1225
+ else if (d.property === "borderColor" && d.codeValue) {
1226
+ item.figmaTool = "figma_set_strokes";
1227
+ item.figmaToolParams = {
1228
+ nodeId,
1229
+ strokes: [{ type: "SOLID", color: String(d.codeValue) }],
1230
+ };
1231
+ }
1232
+ else if (d.property === "borderRadius" && d.codeValue) {
1233
+ item.figmaTool = "figma_execute";
1234
+ item.figmaToolParams = {
1235
+ code: `const node = figma.getNodeById("${nodeId}"); if (node && "cornerRadius" in node) { node.cornerRadius = ${d.codeValue}; }`,
1236
+ };
1237
+ }
1238
+ else if (d.property === "borderWidth" && d.codeValue) {
1239
+ item.figmaTool = "figma_execute";
1240
+ item.figmaToolParams = {
1241
+ code: `const node = figma.getNodeById("${nodeId}"); if (node && "strokeWeight" in node) { node.strokeWeight = ${d.codeValue}; }`,
1242
+ };
1243
+ }
1244
+ break;
1245
+ case "spacing":
1246
+ if (d.codeValue !== null && d.codeValue !== undefined) {
1247
+ const prop = d.property;
1248
+ if (["paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "gap"].includes(prop)) {
1249
+ const figmaProp = prop === "gap" ? "itemSpacing" : prop;
1250
+ item.figmaTool = "figma_execute";
1251
+ item.figmaToolParams = {
1252
+ code: `const node = figma.getNodeById("${nodeId}"); if (node && "${figmaProp}" in node) { node.${figmaProp} = ${d.codeValue}; }`,
1253
+ };
1254
+ }
1255
+ else if (prop === "width" || prop === "height") {
1256
+ item.figmaTool = "figma_resize_node";
1257
+ item.figmaToolParams = {
1258
+ nodeId,
1259
+ [prop]: Number(d.codeValue),
1260
+ };
1261
+ }
1262
+ }
1263
+ break;
1264
+ case "componentAPI":
1265
+ if (d.property.startsWith("prop:") && d.codeValue && !d.designValue) {
1266
+ item.figmaTool = "figma_add_component_property";
1267
+ item.figmaToolParams = {
1268
+ nodeId,
1269
+ propertyName: String(d.codeValue),
1270
+ type: "VARIANT",
1271
+ defaultValue: "",
1272
+ };
1273
+ }
1274
+ break;
1275
+ case "metadata":
1276
+ if (d.property === "description" && d.codeValue) {
1277
+ item.figmaTool = "figma_set_description";
1278
+ item.figmaToolParams = {
1279
+ nodeId,
1280
+ description: String(d.codeValue),
1281
+ };
1282
+ }
1283
+ break;
1284
+ default:
1285
+ // For categories without direct tool mappings, provide a description
1286
+ item.codeChange = {
1287
+ filePath,
1288
+ property: d.property,
1289
+ currentValue: d.designValue,
1290
+ targetValue: d.codeValue,
1291
+ description: d.suggestion || d.message,
1292
+ };
1293
+ break;
1294
+ }
1295
+ // If no figma tool was assigned and no codeChange, add generic guidance
1296
+ if (!item.figmaTool && !item.codeChange) {
1297
+ item.codeChange = {
1298
+ property: d.property,
1299
+ currentValue: d.designValue,
1300
+ targetValue: d.codeValue,
1301
+ description: `Update design: ${d.message}`,
1302
+ };
1303
+ }
1304
+ }
1305
+ else {
1306
+ // Code-side fix
1307
+ item.codeChange = {
1308
+ filePath,
1309
+ property: d.property,
1310
+ currentValue: d.codeValue,
1311
+ targetValue: d.designValue,
1312
+ description: d.suggestion || `Update code to match design: ${d.message}`,
1313
+ };
1314
+ }
1315
+ items.push(item);
1316
+ }
1317
+ return items;
1318
+ }
1319
+ // ============================================================================
1320
+ // Parity Report Presentation Instruction
1321
+ // ============================================================================
1322
+ /**
1323
+ * Build the ai_instruction for parity check results.
1324
+ * Defines a consistent presentation structure so any AI consuming this tool
1325
+ * produces scannable, structured reports with conversational analysis.
1326
+ */
1327
+ function buildParityInstruction(componentName, parityScore, counts, canonicalSource, totalDiscrepancies) {
1328
+ if (totalDiscrepancies === 0) {
1329
+ return [
1330
+ `No discrepancies found for ${componentName}. Design and code are in sync (parity score: 100/100).`,
1331
+ "",
1332
+ "Present as:",
1333
+ "",
1334
+ `## ${componentName} — Parity Report`,
1335
+ `**Score: 100/100** | 0 critical | 0 major | 0 minor | 0 info`,
1336
+ "",
1337
+ "### Action Required",
1338
+ "None — design and code are fully aligned.",
1339
+ "",
1340
+ "### Verdict",
1341
+ "Ready for sign-off. All compared properties match between design and code.",
1342
+ ].join("\n");
1343
+ }
1344
+ return [
1345
+ `Found ${totalDiscrepancies} discrepancies (parity score: ${parityScore}/100) for ${componentName}. Canonical source: '${canonicalSource}'.`,
1346
+ "",
1347
+ "Present results using this consistent structure:",
1348
+ "",
1349
+ `## ${componentName} — Parity Report`,
1350
+ `**Score: ${parityScore}/100** | ${counts.critical} critical | ${counts.major} major | ${counts.minor} minor | ${counts.info} info`,
1351
+ "",
1352
+ "### Action Required",
1353
+ "List discrepancies that represent real spec gaps needing resolution before sign-off.",
1354
+ "For each, state what differs and suggest the fix direction based on the canonical source.",
1355
+ "If no actionable items exist, write: \"None — design and code are aligned on all spec values.\"",
1356
+ "",
1357
+ "### Aligned",
1358
+ "Compact bulleted list of properties that matched between design and code (no discrepancy flagged).",
1359
+ "Include: colors/fills, border, spacing/padding, gap, typography, layout — whichever were compared and matched.",
1360
+ "This builds confidence in what's already correct.",
1361
+ "",
1362
+ "### Notes",
1363
+ "Conversational analysis of remaining items. Explain paradigm differences (e.g., Figma component properties vs React composition slots),",
1364
+ "note missing accessibility annotations, flag metadata differences, and provide editorial recommendations.",
1365
+ "This is where context and judgment live — interpret the findings, don't just list them.",
1366
+ "",
1367
+ "### Verdict",
1368
+ "One sentence: is this component ready for sign-off, does it need minor adjustments, or are there blockers?",
1369
+ "",
1370
+ "Categorization rules:",
1371
+ "- Critical/major discrepancies → always Action Required",
1372
+ "- Minor discrepancies that are real spec gaps (colors, spacing, radius, typography, tokens) → Action Required",
1373
+ "- Minor discrepancies that are paradigm differences (className prop, React-only behavioral props) → Notes",
1374
+ "- Info-level items → always Notes",
1375
+ "- Keep Action Required focused — if a developer only reads one section, this is it",
1376
+ "- The Aligned section should be scannable in under 5 seconds",
1377
+ "- Offer to apply fixes when actionable items exist",
1378
+ ].join("\n");
1379
+ }
1380
+ // ============================================================================
1381
+ // Documentation Section Generators
1382
+ // ============================================================================
1383
+ /**
1384
+ * Detect the atomic-design level (atom | molecule | organism | template) of a component
1385
+ * by finding its Figma page and walking the ordered page list back to the nearest
1386
+ * section-divider page (e.g. "ATOMS", "MOLECULES", "ORGANISMS"). Returns null when the
1387
+ * file doesn't use atomic-design page sections or the page can't be resolved — callers
1388
+ * then simply omit the `level` frontmatter. Best-effort and never throws.
1389
+ */
1390
+ async function detectAtomicLevel(api, fileKey, nodeId, setNodeId, _componentMeta, _allComponentsMeta) {
1391
+ try {
1392
+ const targetId = setNodeId || nodeId;
1393
+ // Resolve the page the component lives on — independent of library-publish
1394
+ // status (published `containing_frame` metadata is empty for many files).
1395
+ // Requesting the file with `ids` returns every page in document order, but
1396
+ // prunes each page's children to only the path reaching the requested node,
1397
+ // so the single page whose subtree still contains the node is its home page.
1398
+ const pages = (await api.getFile(fileKey, { ids: [targetId] }))?.document?.children || [];
1399
+ const contains = (n) => n?.id === targetId || (Array.isArray(n?.children) && n.children.some(contains));
1400
+ const idx = pages.findIndex((p) => contains(p));
1401
+ if (idx < 0)
1402
+ return null;
1403
+ // Walk back to the nearest atomic-design divider page.
1404
+ const LEVELS = [
1405
+ ["ATOM", "atom"],
1406
+ ["MOLECULE", "molecule"],
1407
+ ["ORGANISM", "organism"],
1408
+ ["TEMPLATE", "template"],
1409
+ ];
1410
+ for (let i = idx; i >= 0; i--) {
1411
+ const stripped = (pages[i]?.name || "").toUpperCase().replace(/[^A-Z]/g, "");
1412
+ for (const [marker, level] of LEVELS) {
1413
+ if (stripped.startsWith(marker))
1414
+ return level;
1415
+ }
1416
+ }
1417
+ return null;
1418
+ }
1419
+ catch {
1420
+ return null;
1421
+ }
1422
+ }
1423
+ function generateFrontmatter(componentName, description, node, componentMeta, fileUrl, codeInfo, canonicalSource, level) {
1424
+ const status = codeInfo?.changelog?.[0]
1425
+ ? "stable"
1426
+ : componentMeta?.description?.toLowerCase().includes("deprecated")
1427
+ ? "deprecated"
1428
+ : "stable";
1429
+ const version = codeInfo?.changelog?.[0]?.version || "1.0.0";
1430
+ const tags = [componentName.toLowerCase()];
1431
+ if (level)
1432
+ tags.push(level);
1433
+ if (node.type === "COMPONENT_SET")
1434
+ tags.push("variants");
1435
+ if (node.componentPropertyDefinitions)
1436
+ tags.push("configurable");
1437
+ const lines = [
1438
+ "---",
1439
+ `title: ${componentName}`,
1440
+ `description: ${((description.split(/\n\s*\n|\n#{1,6}\s|\n\*\*/)[0] || description).replace(/\n/g, " ").replace(/\s+/g, " ").trim().split(/(?<=[.!?])\s+/)[0] || `${componentName} component`)}`,
1441
+ `status: ${status}`,
1442
+ `version: ${version}`,
1443
+ `category: components`,
1444
+ ...(level ? [`level: ${level}`] : []),
1445
+ `tags: [${tags.join(", ")}]`,
1446
+ `figma: ${fileUrl}`,
1447
+ ];
1448
+ if (codeInfo?.filePath) {
1449
+ lines.push(`source: ${codeInfo.filePath}`);
1450
+ }
1451
+ if (codeInfo?.packageName) {
1452
+ lines.push(`package: ${codeInfo.packageName}`);
1453
+ }
1454
+ if (canonicalSource) {
1455
+ lines.push(`canonical: ${canonicalSource}`);
1456
+ }
1457
+ lines.push(`lastUpdated: ${new Date().toISOString().split("T")[0]}`);
1458
+ lines.push("---");
1459
+ return lines.join("\n");
1460
+ }
1461
+ function generateOverviewSection(componentName, description, fileUrl, parsedDesc, codeInfo) {
1462
+ const lines = [
1463
+ `# ${componentName}`,
1464
+ "",
1465
+ ];
1466
+ // Build links line
1467
+ const links = [`**[Open in Figma](${fileUrl})**`];
1468
+ if (codeInfo?.filePath) {
1469
+ links.push(`**[View Source](${codeInfo.filePath})**`);
1470
+ }
1471
+ // Add Storybook link if stories file exists in sourceFiles
1472
+ const storiesFile = codeInfo?.sourceFiles?.find((f) => f.role.toLowerCase().includes("storybook") || f.role.toLowerCase().includes("stories") || f.path.includes(".stories."));
1473
+ if (storiesFile) {
1474
+ links.push(`**[Storybook](${storiesFile.path})**`);
1475
+ }
1476
+ lines.push(links.join(" | "));
1477
+ lines.push("");
1478
+ lines.push("## Overview");
1479
+ lines.push("");
1480
+ // Use parsed overview or fall back to raw description
1481
+ const overviewText = parsedDesc.overview || description?.split("\n")[0] || `The ${componentName} component.`;
1482
+ lines.push(overviewText);
1483
+ lines.push("");
1484
+ // Base component attribution
1485
+ if (codeInfo?.baseComponent) {
1486
+ const baseLink = codeInfo.baseComponent.url
1487
+ ? `[${codeInfo.baseComponent.name}](${codeInfo.baseComponent.url})`
1488
+ : codeInfo.baseComponent.name;
1489
+ if (codeInfo.baseComponent.description) {
1490
+ lines.push(`Built on ${baseLink}, ${codeInfo.baseComponent.description}`);
1491
+ }
1492
+ else {
1493
+ lines.push(`Built on ${baseLink}.`);
1494
+ }
1495
+ lines.push("");
1496
+ }
1497
+ // When to Use
1498
+ if (parsedDesc.whenToUse.length > 0) {
1499
+ lines.push("### When to Use");
1500
+ lines.push("");
1501
+ for (const item of parsedDesc.whenToUse) {
1502
+ lines.push(`- ${item}`);
1503
+ }
1504
+ lines.push("");
1505
+ }
1506
+ // When NOT to Use
1507
+ if (parsedDesc.whenNotToUse.length > 0) {
1508
+ lines.push("### When NOT to Use");
1509
+ lines.push("");
1510
+ for (const item of parsedDesc.whenNotToUse) {
1511
+ lines.push(`- ${item}`);
1512
+ }
1513
+ lines.push("");
1514
+ }
1515
+ return lines.join("\n");
1516
+ }
1517
+ function generateStatesAndVariantsSection(node, variantData) {
1518
+ const props = node.componentPropertyDefinitions;
1519
+ if (!props || Object.keys(props).length === 0)
1520
+ return "";
1521
+ const lines = ["", "## Variants", ""];
1522
+ const variants = [];
1523
+ const booleans = [];
1524
+ const textProps = [];
1525
+ for (const [rawName, def] of Object.entries(props)) {
1526
+ // Strip Figma internal ID suffixes like "#17100:0" from property names
1527
+ const name = rawName.replace(/#\d+:\d+$/, "").trim();
1528
+ if (def.type === "VARIANT") {
1529
+ variants.push({
1530
+ name,
1531
+ values: def.variantOptions || [],
1532
+ defaultValue: def.defaultValue || "",
1533
+ });
1534
+ }
1535
+ else if (def.type === "BOOLEAN") {
1536
+ booleans.push({ name, defaultValue: def.defaultValue ?? true });
1537
+ }
1538
+ else if (def.type === "TEXT") {
1539
+ textProps.push({ name, defaultValue: def.defaultValue || "" });
1540
+ }
1541
+ }
1542
+ // Variant matrix with per-variant color data
1543
+ if (variants.length > 0 && variantData && variantData.length > 0) {
1544
+ lines.push("### Variant Matrix");
1545
+ lines.push("");
1546
+ // Determine which columns to show based on available data
1547
+ const hasIcons = variantData.some((v) => v.icons.length > 0);
1548
+ const hasFills = variantData.some((v) => v.fills.length > 0);
1549
+ if (hasFills || hasIcons) {
1550
+ const headerParts = ["Variant", "Background"];
1551
+ if (hasIcons)
1552
+ headerParts.push("Icon");
1553
+ headerParts.push("Text/Icon Color");
1554
+ lines.push("| " + headerParts.join(" | ") + " |");
1555
+ const separatorParts = ["--------", "----------"];
1556
+ if (hasIcons)
1557
+ separatorParts.push("----");
1558
+ separatorParts.push("---------------");
1559
+ lines.push("|" + separatorParts.join("|") + "|");
1560
+ for (const vd of variantData) {
1561
+ const displayName = cleanVariantName(vd.variantName);
1562
+ // Get primary fill (background)
1563
+ const bgFill = vd.fills[0];
1564
+ const bgVal = bgFill
1565
+ ? (bgFill.variableName ? `\`${bgFill.variableName}\` (${bgFill.hex})` : bgFill.hex)
1566
+ : "—";
1567
+ // Get primary text/icon color
1568
+ const textColor = vd.textColors[0] || vd.strokes[0];
1569
+ const textVal = textColor
1570
+ ? (textColor.variableName ? `\`${textColor.variableName}\` (${textColor.hex})` : textColor.hex)
1571
+ : "—";
1572
+ const rowParts = [`**${displayName}**`, bgVal];
1573
+ if (hasIcons) {
1574
+ const icon = vd.icons[0]?.name || "—";
1575
+ rowParts.push(icon);
1576
+ }
1577
+ rowParts.push(textVal);
1578
+ lines.push("| " + rowParts.join(" | ") + " |");
1579
+ }
1580
+ lines.push("");
1581
+ }
1582
+ // Icon-to-variant mapping table
1583
+ if (hasIcons) {
1584
+ lines.push("### Icon Mapping");
1585
+ lines.push("");
1586
+ lines.push("| Variant | Figma Icon Instance |");
1587
+ lines.push("|---------|---------------------|");
1588
+ for (const vd of variantData) {
1589
+ const displayName = cleanVariantName(vd.variantName);
1590
+ const icon = vd.icons[0]?.name || "—";
1591
+ lines.push(`| ${displayName} | ${icon} |`);
1592
+ }
1593
+ lines.push("");
1594
+ }
1595
+ }
1596
+ else if (variants.length > 0) {
1597
+ // Fallback: simple variant table without color data
1598
+ lines.push("| Variant | Values | Default |");
1599
+ lines.push("|---------|--------|---------|");
1600
+ for (const v of variants) {
1601
+ lines.push(`| ${v.name} | ${v.values.join(", ")} | ${v.defaultValue} |`);
1602
+ }
1603
+ lines.push("");
1604
+ }
1605
+ // Configurable properties table (all property types)
1606
+ if (booleans.length > 0 || textProps.length > 0) {
1607
+ lines.push("### Configurable Properties");
1608
+ lines.push("");
1609
+ lines.push("| Property | Type | Default | Description |");
1610
+ lines.push("|----------|------|---------|-------------|");
1611
+ for (const v of variants) {
1612
+ lines.push(`| **${v.name}** | \`${v.values.map((val) => `"${val}"`).join(" \\| ")}\` | \`"${v.defaultValue}"\` | Changes visual treatment |`);
1613
+ }
1614
+ for (const b of booleans) {
1615
+ lines.push(`| **${b.name}** | \`boolean\` | \`${b.defaultValue}\` | Shows/hides ${b.name.toLowerCase()} element |`);
1616
+ }
1617
+ for (const t of textProps) {
1618
+ lines.push(`| **${t.name}** | \`string\` | \`"${t.defaultValue}"\` | Sets ${t.name.toLowerCase()} content |`);
1619
+ }
1620
+ lines.push("");
1621
+ }
1622
+ return lines.join("\n");
1623
+ }
1624
+ /**
1625
+ * Recursively walk a node tree and collect all unique colors from fills, strokes, and text.
1626
+ * For COMPONENT_SET nodes, walks the first child (default variant) instead of the set frame.
1627
+ */
1628
+ function collectNodeColors(node, colors, depth = 0, maxDepth = 3) {
1629
+ if (depth > maxDepth)
1630
+ return;
1631
+ // For COMPONENT_SET, walk into the first child (default variant) for visual data
1632
+ if (node.type === "COMPONENT_SET" && node.children?.length > 0 && depth === 0) {
1633
+ collectNodeColors(node.children[0], colors, 0, maxDepth);
1634
+ return;
1635
+ }
1636
+ const isText = node.type === "TEXT";
1637
+ // Fills
1638
+ if (node.fills && Array.isArray(node.fills)) {
1639
+ for (const fill of node.fills) {
1640
+ if (fill.type === "SOLID" && fill.color && fill.visible !== false) {
1641
+ const hex = figmaRGBAToHex({ ...fill.color, a: fill.opacity ?? fill.color.a ?? 1 });
1642
+ colors.push({
1643
+ hex,
1644
+ property: isText ? "text" : "fill",
1645
+ nodeName: node.name || "",
1646
+ variableId: fill.boundVariables?.color?.id,
1647
+ });
1648
+ }
1649
+ }
1650
+ }
1651
+ // Strokes
1652
+ if (node.strokes && Array.isArray(node.strokes)) {
1653
+ for (const stroke of node.strokes) {
1654
+ if (stroke.type === "SOLID" && stroke.color && stroke.visible !== false) {
1655
+ const hex = figmaRGBAToHex({ ...stroke.color, a: stroke.opacity ?? stroke.color.a ?? 1 });
1656
+ colors.push({
1657
+ hex,
1658
+ property: "stroke",
1659
+ nodeName: node.name || "",
1660
+ variableId: stroke.boundVariables?.color?.id,
1661
+ });
1662
+ }
1663
+ }
1664
+ }
1665
+ // Recurse into children
1666
+ if (node.children && Array.isArray(node.children)) {
1667
+ for (const child of node.children) {
1668
+ collectNodeColors(child, colors, depth + 1, maxDepth);
1669
+ }
1670
+ }
1671
+ }
1672
+ /** Deduplicate collected colors, keeping the most descriptive context for each unique hex */
1673
+ function deduplicateColors(colors) {
1674
+ const seen = new Map();
1675
+ for (const c of colors) {
1676
+ const key = `${c.hex}:${c.property}`;
1677
+ if (!seen.has(key)) {
1678
+ seen.set(key, c);
1679
+ }
1680
+ }
1681
+ return Array.from(seen.values());
1682
+ }
1683
+ function generateVisualSpecsSection(node, enrichedData, variantData) {
1684
+ const lines = ["", "## Token Specification", ""];
1685
+ // Build variable name lookup from enrichment data
1686
+ const varNameMap = new Map();
1687
+ if (enrichedData?.variables_used) {
1688
+ for (const v of enrichedData.variables_used) {
1689
+ varNameMap.set(v.id, v.name);
1690
+ }
1691
+ }
1692
+ // Per-variant color token table
1693
+ if (variantData && variantData.length > 0) {
1694
+ lines.push("### Color Tokens");
1695
+ lines.push("");
1696
+ lines.push("| Element | Figma Variable | Value |");
1697
+ lines.push("|---------|---------------|-------|");
1698
+ for (const vd of variantData) {
1699
+ const nameMatch = vd.variantName.match(/Variant=([^,]+)/i);
1700
+ const displayName = nameMatch ? nameMatch[1].trim() : vd.variantName;
1701
+ // Section header for this variant
1702
+ lines.push(`| **${displayName}** | | |`);
1703
+ // Background fills
1704
+ for (const fill of vd.fills) {
1705
+ const varName = fill.variableName || (fill.variableId ? varNameMap.get(fill.variableId) : undefined);
1706
+ lines.push(`| Background | ${varName ? `\`${varName}\`` : "—"} | ${fill.hex} |`);
1707
+ }
1708
+ // Text colors
1709
+ for (const text of vd.textColors) {
1710
+ const varName = text.variableName || (text.variableId ? varNameMap.get(text.variableId) : undefined);
1711
+ lines.push(`| Text (${text.nodeName}) | ${varName ? `\`${varName}\`` : "—"} | ${text.hex} |`);
1712
+ }
1713
+ // Strokes
1714
+ for (const stroke of vd.strokes) {
1715
+ const varName = stroke.variableName || (stroke.variableId ? varNameMap.get(stroke.variableId) : undefined);
1716
+ lines.push(`| Stroke | ${varName ? `\`${varName}\`` : "—"} | ${stroke.hex} |`);
1717
+ }
1718
+ }
1719
+ lines.push("");
1720
+ }
1721
+ else {
1722
+ // Fallback: collect from default variant
1723
+ const allColors = [];
1724
+ collectNodeColors(node, allColors);
1725
+ const uniqueColors = deduplicateColors(allColors);
1726
+ if (uniqueColors.length > 0) {
1727
+ lines.push("### Colors & Fills");
1728
+ lines.push("| Property | Element | Value |");
1729
+ lines.push("|----------|---------|-------|");
1730
+ const order = { fill: 0, text: 1, stroke: 2 };
1731
+ uniqueColors.sort((a, b) => order[a.property] - order[b.property]);
1732
+ for (const c of uniqueColors) {
1733
+ const label = c.property === "fill" ? "Fill" : c.property === "text" ? "Text" : "Stroke";
1734
+ const tokenName = c.variableId ? varNameMap.get(c.variableId) : undefined;
1735
+ const value = tokenName ? `${c.hex} (\`${tokenName}\`)` : c.hex;
1736
+ lines.push(`| ${label} | ${c.nodeName} | ${value} |`);
1737
+ }
1738
+ lines.push("");
1739
+ }
1740
+ }
1741
+ // Spacing tokens with variable names
1742
+ const visualNode = resolveVisualNode(node);
1743
+ const spacingTokens = collectSpacingTokens(visualNode);
1744
+ if (spacingTokens.length > 0) {
1745
+ lines.push("### Spacing Tokens");
1746
+ lines.push("");
1747
+ lines.push("| Property | Figma Variable | Value |");
1748
+ lines.push("|----------|---------------|-------|");
1749
+ for (const token of spacingTokens) {
1750
+ const varDisplay = token.variableName ? `\`${token.variableName}\`` : "—";
1751
+ lines.push(`| ${token.property} | ${varDisplay} | ${token.value}px |`);
1752
+ }
1753
+ lines.push("");
1754
+ }
1755
+ else {
1756
+ // Fallback: simple spacing output
1757
+ const spacing = extractSpacingProperties(visualNode);
1758
+ const hasSpacing = Object.keys(spacing).length > 0;
1759
+ if (hasSpacing) {
1760
+ lines.push("### Spacing & Layout");
1761
+ if (spacing.paddingTop !== undefined || spacing.paddingRight !== undefined) {
1762
+ lines.push(`- Padding: ${spacing.paddingTop ?? 0}px ${spacing.paddingRight ?? 0}px ${spacing.paddingBottom ?? 0}px ${spacing.paddingLeft ?? 0}px`);
1763
+ }
1764
+ if (spacing.gap !== undefined)
1765
+ lines.push(`- Gap: ${spacing.gap}px`);
1766
+ lines.push("");
1767
+ }
1768
+ if (visualNode.cornerRadius !== undefined) {
1769
+ lines.push(`- Border Radius: ${visualNode.cornerRadius}px`);
1770
+ lines.push("");
1771
+ }
1772
+ }
1773
+ // Token coverage
1774
+ if (enrichedData?.token_coverage !== undefined) {
1775
+ lines.push("### Token Coverage");
1776
+ lines.push(`Score: ${enrichedData.token_coverage}%`);
1777
+ if (enrichedData.hardcoded_values && enrichedData.hardcoded_values.length > 0) {
1778
+ lines.push(`| Hardcoded values: ${enrichedData.hardcoded_values.length}`);
1779
+ }
1780
+ lines.push("");
1781
+ }
1782
+ return lines.join("\n");
1783
+ }
1784
+ function generateImplementationSection(codeInfo) {
1785
+ if (!codeInfo)
1786
+ return "";
1787
+ const lines = ["", "## Implementation", ""];
1788
+ // Source files table
1789
+ if (codeInfo.sourceFiles && codeInfo.sourceFiles.length > 0) {
1790
+ lines.push("### Source Files");
1791
+ lines.push("");
1792
+ lines.push("| File | Role | Variants |");
1793
+ lines.push("|------|------|----------|");
1794
+ for (const sf of codeInfo.sourceFiles) {
1795
+ lines.push(`| \`${sf.path}\` | ${sf.role} | ${sf.variants ?? "—"} |`);
1796
+ }
1797
+ lines.push("");
1798
+ }
1799
+ // Import statement
1800
+ if (codeInfo.importStatement) {
1801
+ lines.push("### Import");
1802
+ lines.push("");
1803
+ lines.push("```tsx");
1804
+ lines.push(codeInfo.importStatement);
1805
+ lines.push("```");
1806
+ lines.push("");
1807
+ }
1808
+ // CVA / variant definition
1809
+ if (codeInfo.variantDefinition) {
1810
+ lines.push("### Variant Definition");
1811
+ lines.push("");
1812
+ lines.push("```tsx");
1813
+ lines.push(codeInfo.variantDefinition);
1814
+ lines.push("```");
1815
+ lines.push("");
1816
+ }
1817
+ // Component API - main props
1818
+ if (codeInfo.props && codeInfo.props.length > 0) {
1819
+ lines.push("### Component API");
1820
+ lines.push("");
1821
+ lines.push("| Prop | Type | Default | Description |");
1822
+ lines.push("|------|------|---------|-------------|");
1823
+ for (const p of codeInfo.props) {
1824
+ lines.push(`| \`${p.name}\` | \`${p.type.replace(/\|/g, "\\|")}\` | ${p.defaultValue ? `\`${p.defaultValue}\`` : "—"} | ${p.description ?? "—"} |`);
1825
+ }
1826
+ lines.push("");
1827
+ }
1828
+ // Sub-component APIs
1829
+ if (codeInfo.subComponents && codeInfo.subComponents.length > 0) {
1830
+ for (const sub of codeInfo.subComponents) {
1831
+ lines.push(`#### ${sub.name}`);
1832
+ lines.push("");
1833
+ if (sub.description) {
1834
+ lines.push(sub.description);
1835
+ lines.push("");
1836
+ }
1837
+ if (sub.element) {
1838
+ lines.push(`Renders a \`<${sub.element}>\`${sub.dataSlot ? ` with \`data-slot="${sub.dataSlot}"\`` : ""}.`);
1839
+ lines.push("");
1840
+ }
1841
+ if (sub.props && sub.props.length > 0) {
1842
+ lines.push("| Prop | Type | Default | Description |");
1843
+ lines.push("|------|------|---------|-------------|");
1844
+ for (const p of sub.props) {
1845
+ lines.push(`| \`${p.name}\` | \`${p.type.replace(/\|/g, "\\|")}\` | ${p.defaultValue ? `\`${p.defaultValue}\`` : "—"} | ${p.description ?? "—"} |`);
1846
+ }
1847
+ lines.push("");
1848
+ }
1849
+ }
1850
+ }
1851
+ // Events
1852
+ if (codeInfo.events && codeInfo.events.length > 0) {
1853
+ lines.push("### Events");
1854
+ lines.push("");
1855
+ lines.push("| Event | Payload | Description |");
1856
+ lines.push("|-------|---------|-------------|");
1857
+ for (const e of codeInfo.events) {
1858
+ lines.push(`| ${e.name} | ${e.payload ?? "—"} | ${e.description ?? "—"} |`);
1859
+ }
1860
+ lines.push("");
1861
+ }
1862
+ // Slots
1863
+ if (codeInfo.slots && codeInfo.slots.length > 0) {
1864
+ lines.push("### Slots");
1865
+ lines.push("");
1866
+ for (const s of codeInfo.slots) {
1867
+ lines.push(`- **${s.name}**: ${s.description ?? ""}`);
1868
+ }
1869
+ lines.push("");
1870
+ }
1871
+ // Usage examples
1872
+ if (codeInfo.usageExamples && codeInfo.usageExamples.length > 0) {
1873
+ lines.push("### Usage Examples");
1874
+ lines.push("");
1875
+ for (const ex of codeInfo.usageExamples) {
1876
+ lines.push(`#### ${ex.title}`);
1877
+ lines.push("");
1878
+ lines.push(`\`\`\`${ex.language || "tsx"}`);
1879
+ lines.push(ex.code);
1880
+ lines.push("```");
1881
+ lines.push("");
1882
+ }
1883
+ }
1884
+ return lines.join("\n");
1885
+ }
1886
+ function generateAccessibilitySection(node, parsedDesc, codeInfo) {
1887
+ const lines = ["", "## Accessibility", ""];
1888
+ // Use structured accessibility notes from parsed description
1889
+ if (parsedDesc.accessibilityNotes.length > 0) {
1890
+ for (const note of parsedDesc.accessibilityNotes) {
1891
+ lines.push(`- ${note}`);
1892
+ }
1893
+ lines.push("");
1894
+ }
1895
+ else {
1896
+ // Fallback: check raw description for accessibility content
1897
+ const description = node.descriptionMarkdown || node.description || "";
1898
+ if (description.toLowerCase().includes("aria") || description.toLowerCase().includes("accessibility")) {
1899
+ // Extract accessibility-related lines from description
1900
+ const descLines = description.split("\n");
1901
+ for (const line of descLines) {
1902
+ const lower = line.toLowerCase();
1903
+ if (lower.includes("aria") || lower.includes("accessibility") || lower.includes("screen reader") || lower.includes("keyboard") || lower.includes("focus") || lower.includes("wcag") || lower.includes("contrast")) {
1904
+ lines.push(`- ${line.trim().replace(/^[-*•]\s*/, "")}`);
1905
+ }
1906
+ }
1907
+ if (lines.length > 2) {
1908
+ lines.push("");
1909
+ }
1910
+ else {
1911
+ lines.push("_Accessibility mentions found in description but not in structured format. Review the component description for details._");
1912
+ lines.push("");
1913
+ }
1914
+ }
1915
+ else {
1916
+ lines.push("_No accessibility annotations found in Figma. Add annotations to the component description._");
1917
+ lines.push("");
1918
+ }
1919
+ }
1920
+ return lines.join("\n");
1921
+ }
1922
+ function generateDesignAnnotationsSection(node) {
1923
+ // Annotations come from the Desktop Bridge plugin (node.annotations)
1924
+ // They are available when the component was fetched via Desktop Bridge
1925
+ const annotations = node.annotations || [];
1926
+ if (annotations.length === 0) {
1927
+ return [
1928
+ "",
1929
+ "## Design Annotations",
1930
+ "",
1931
+ "_No design annotations found on this node. Designers can add annotations in Dev Mode to specify animation timings, easing curves, interaction behaviors, and other implementation details._",
1932
+ "_Use `figma_get_annotations` with `include_children=true` to check child nodes for annotations._",
1933
+ "",
1934
+ ].join("\n");
1935
+ }
1936
+ const lines = ["", "## Design Annotations", ""];
1937
+ lines.push(`Found **${annotations.length}** annotation(s) on this component:`, "");
1938
+ for (let i = 0; i < annotations.length; i++) {
1939
+ const ann = annotations[i];
1940
+ const num = i + 1;
1941
+ // Header with category if available
1942
+ const categoryLabel = ann.categoryName ? ` (${ann.categoryName})` : (ann.categoryId ? ` (category: ${ann.categoryId})` : "");
1943
+ lines.push(`### Annotation ${num}${categoryLabel}`);
1944
+ lines.push("");
1945
+ // Label content
1946
+ if (ann.labelMarkdown) {
1947
+ // Indent markdown content and render it
1948
+ lines.push(ann.labelMarkdown);
1949
+ lines.push("");
1950
+ }
1951
+ else if (ann.label) {
1952
+ lines.push(ann.label);
1953
+ lines.push("");
1954
+ }
1955
+ // Pinned properties
1956
+ if (ann.properties && ann.properties.length > 0) {
1957
+ lines.push("**Pinned Properties:**");
1958
+ for (const prop of ann.properties) {
1959
+ lines.push(`- \`${prop.type}\``);
1960
+ }
1961
+ lines.push("");
1962
+ }
1963
+ }
1964
+ return lines.join("\n");
1965
+ }
1966
+ function generateChangelogSection(codeInfo) {
1967
+ if (!codeInfo?.changelog || codeInfo.changelog.length === 0)
1968
+ return "";
1969
+ const lines = ["", "## Changelog", ""];
1970
+ lines.push("| Version | Date | Changes |");
1971
+ lines.push("|---------|------|---------|");
1972
+ for (const entry of codeInfo.changelog) {
1973
+ lines.push(`| ${entry.version} | ${entry.date} | ${entry.changes} |`);
1974
+ }
1975
+ lines.push("");
1976
+ return lines.join("\n");
1977
+ }
1978
+ // ============================================================================
1979
+ // New Section Generators (Anatomy, Typography, Content Guidelines, Parity)
1980
+ // ============================================================================
1981
+ function generateAnatomySection(node) {
1982
+ const lines = ["", "## Component Anatomy", ""];
1983
+ // For COMPONENT_SET, list all variants first
1984
+ if (node.type === "COMPONENT_SET" && node.children?.length > 0) {
1985
+ lines.push(`**${node.children.length} variants:**`);
1986
+ for (const child of node.children) {
1987
+ lines.push(`- ${cleanVariantName(child.name || "Unknown")}`);
1988
+ }
1989
+ lines.push("");
1990
+ }
1991
+ lines.push("### Design Structure (Figma)");
1992
+ lines.push("");
1993
+ const tree = buildAnatomyTree(node);
1994
+ if (tree.includes("└── ") || tree.includes("├── ")) {
1995
+ // Rich tree with children
1996
+ lines.push("```");
1997
+ lines.push(tree);
1998
+ lines.push("```");
1999
+ }
2000
+ else {
2001
+ // Shallow tree (REST API depth limitation)
2002
+ lines.push("```");
2003
+ lines.push(tree);
2004
+ lines.push("```");
2005
+ lines.push("");
2006
+ lines.push("_Note: Tree depth may be limited by the Figma REST API. Use the Desktop Bridge plugin for full node-level anatomy._");
2007
+ }
2008
+ lines.push("");
2009
+ return lines.join("\n");
2010
+ }
2011
+ function generateTypographySection(node) {
2012
+ const textStyles = collectTypographyData(node);
2013
+ if (textStyles.length === 0)
2014
+ return "";
2015
+ const lines = ["", "## Typography", ""];
2016
+ lines.push("| Element | Font | Weight | Size | Line Height | Letter Spacing |");
2017
+ lines.push("|---------|------|--------|------|-------------|----------------|");
2018
+ // Deduplicate by font properties
2019
+ const seen = new Set();
2020
+ for (const ts of textStyles) {
2021
+ const key = `${ts.fontFamily}:${ts.fontWeight}:${ts.fontSize}:${ts.lineHeight}`;
2022
+ if (seen.has(key))
2023
+ continue;
2024
+ seen.add(key);
2025
+ lines.push(`| ${ts.nodeName} | ${ts.fontFamily} | ${ts.fontWeightName} (${ts.fontWeight}) | ${ts.fontSize}px | ${ts.lineHeight}px | ${ts.letterSpacing === 0 ? "0" : `${ts.letterSpacing}px`} |`);
2026
+ }
2027
+ lines.push("");
2028
+ return lines.join("\n");
2029
+ }
2030
+ function generateContentGuidelinesSection(parsedDesc) {
2031
+ if (parsedDesc.contentGuidelines.length === 0 && parsedDesc.additionalNotes.length === 0)
2032
+ return "";
2033
+ const lines = ["", "## Content Guidelines", ""];
2034
+ for (const section of parsedDesc.contentGuidelines) {
2035
+ lines.push(`### ${section.heading}`);
2036
+ lines.push("");
2037
+ for (const item of section.items) {
2038
+ lines.push(`- ${item}`);
2039
+ }
2040
+ lines.push("");
2041
+ }
2042
+ if (parsedDesc.additionalNotes.length > 0 && parsedDesc.contentGuidelines.length === 0) {
2043
+ for (const note of parsedDesc.additionalNotes) {
2044
+ lines.push(`- ${note}`);
2045
+ }
2046
+ lines.push("");
2047
+ }
2048
+ return lines.join("\n");
2049
+ }
2050
+ function generateParitySection(node, codeInfo) {
2051
+ const lines = ["", "## Design-Code Parity", ""];
2052
+ // Variant coverage - compare Figma variants with code variants if available
2053
+ // Use case-insensitive comparison: Figma uses "Default", code uses "default"
2054
+ const figmaVariantsRaw = new Map(); // lowercase → original name
2055
+ if (node.type === "COMPONENT_SET" && node.children) {
2056
+ for (const child of node.children) {
2057
+ const match = child.name?.match(/Variant=([^,]+)/i) || child.name?.match(/^([^,=]+)/);
2058
+ if (match) {
2059
+ const raw = match[1].trim();
2060
+ figmaVariantsRaw.set(raw.toLowerCase(), raw);
2061
+ }
2062
+ }
2063
+ }
2064
+ // Try to extract code variants from variant definition or props
2065
+ const codeVariantsRaw = new Map(); // lowercase → original name
2066
+ if (codeInfo.props) {
2067
+ const variantProp = codeInfo.props.find((p) => p.name.toLowerCase() === "variant");
2068
+ if (variantProp?.type) {
2069
+ // Match both single and double quoted values: "default" or 'default'
2070
+ const matches = variantProp.type.match(/["']([^"']+)["']/g);
2071
+ if (matches) {
2072
+ for (const m of matches) {
2073
+ const raw = m.replace(/["']/g, "");
2074
+ codeVariantsRaw.set(raw.toLowerCase(), raw);
2075
+ }
2076
+ }
2077
+ }
2078
+ }
2079
+ if (figmaVariantsRaw.size > 0 || codeVariantsRaw.size > 0) {
2080
+ // Merge by lowercase key
2081
+ const allKeys = new Set([...figmaVariantsRaw.keys(), ...codeVariantsRaw.keys()]);
2082
+ lines.push("### Variant Coverage");
2083
+ lines.push("");
2084
+ lines.push("| Variant | In Figma | In Code | Status |");
2085
+ lines.push("|---------|----------|---------|--------|");
2086
+ for (const key of allKeys) {
2087
+ const figmaName = figmaVariantsRaw.get(key);
2088
+ const codeName = codeVariantsRaw.get(key);
2089
+ const displayName = figmaName || codeName || key;
2090
+ const inFigma = figmaVariantsRaw.has(key);
2091
+ const inCode = codeVariantsRaw.has(key);
2092
+ let status;
2093
+ if (inFigma && inCode) {
2094
+ status = "In sync";
2095
+ }
2096
+ else if (inFigma && !inCode) {
2097
+ status = "Figma-only — needs code variant";
2098
+ }
2099
+ else {
2100
+ status = "Code-only — needs Figma variant";
2101
+ }
2102
+ lines.push(`| ${displayName} | ${inFigma ? "Yes" : "**No**"} | ${inCode ? "Yes" : "**No**"} | ${status} |`);
2103
+ }
2104
+ lines.push("");
2105
+ }
2106
+ return lines.join("\n");
2107
+ }
2108
+ // ============================================================================
2109
+ // CompanyDocsMCP Helper
2110
+ // ============================================================================
2111
+ /** Convert generated markdown into a CompanyDocsMCP-compatible content entry */
2112
+ export function toCompanyDocsEntry(markdown, componentName, figmaUrl, systemName) {
2113
+ return {
2114
+ title: componentName,
2115
+ content: markdown,
2116
+ category: "components",
2117
+ tags: [componentName.toLowerCase(), "design-system", "component"],
2118
+ metadata: {
2119
+ source: "figma-console-mcp",
2120
+ figmaUrl,
2121
+ systemName,
2122
+ generatedAt: new Date().toISOString(),
2123
+ },
2124
+ };
2125
+ }
2126
+ // ============================================================================
2127
+ // Zod Schemas
2128
+ // ============================================================================
2129
+ const codeSpecSchema = z.object({
2130
+ filePath: z.string().optional().describe("Path to the component source file"),
2131
+ visual: z.object({
2132
+ backgroundColor: z.string().optional(),
2133
+ borderColor: z.string().optional(),
2134
+ borderWidth: z.number().optional(),
2135
+ borderRadius: z.union([z.number(), z.string()]).optional(),
2136
+ opacity: z.number().optional(),
2137
+ fills: z.array(z.object({ color: z.string().optional(), opacity: z.number().optional() })).optional(),
2138
+ strokes: z.array(z.object({ color: z.string().optional(), width: z.number().optional() })).optional(),
2139
+ effects: z.array(z.object({
2140
+ type: z.string(),
2141
+ color: z.string().optional(),
2142
+ offset: z.object({ x: z.number(), y: z.number() }).optional(),
2143
+ blur: z.number().optional(),
2144
+ })).optional(),
2145
+ }).optional().describe("Visual properties from code (colors, borders, effects)"),
2146
+ spacing: z.object({
2147
+ paddingTop: z.number().optional(),
2148
+ paddingRight: z.number().optional(),
2149
+ paddingBottom: z.number().optional(),
2150
+ paddingLeft: z.number().optional(),
2151
+ gap: z.number().optional(),
2152
+ width: z.union([z.number(), z.string()]).optional(),
2153
+ height: z.union([z.number(), z.string()]).optional(),
2154
+ minWidth: z.number().optional(),
2155
+ minHeight: z.number().optional(),
2156
+ maxWidth: z.number().optional(),
2157
+ maxHeight: z.number().optional(),
2158
+ layoutDirection: z.enum(["horizontal", "vertical"]).optional(),
2159
+ }).optional().describe("Spacing and layout properties from code"),
2160
+ typography: z.object({
2161
+ fontFamily: z.string().optional(),
2162
+ fontSize: z.number().optional(),
2163
+ fontWeight: z.union([z.number(), z.string()]).optional(),
2164
+ lineHeight: z.union([z.number(), z.string()]).optional(),
2165
+ letterSpacing: z.number().optional(),
2166
+ textAlign: z.string().optional(),
2167
+ textDecoration: z.string().optional(),
2168
+ textTransform: z.string().optional(),
2169
+ }).optional().describe("Typography properties from code"),
2170
+ tokens: z.object({
2171
+ usedTokens: z.array(z.string()).optional(),
2172
+ hardcodedValues: z.array(z.object({
2173
+ property: z.string(),
2174
+ value: z.union([z.string(), z.number()]),
2175
+ })).optional(),
2176
+ tokenPrefix: z.string().optional(),
2177
+ }).optional().describe("Design token usage in code"),
2178
+ componentAPI: z.object({
2179
+ props: z.array(z.object({
2180
+ name: z.string(),
2181
+ type: z.string(),
2182
+ required: z.boolean().optional(),
2183
+ defaultValue: z.union([z.string(), z.number(), z.boolean()]).optional(),
2184
+ description: z.string().optional(),
2185
+ values: z.array(z.string()).optional(),
2186
+ })).optional(),
2187
+ events: z.array(z.string()).optional(),
2188
+ slots: z.array(z.string()).optional(),
2189
+ }).optional().describe("Component API (props, events, slots)"),
2190
+ accessibility: z.object({
2191
+ role: z.string().optional(),
2192
+ ariaLabel: z.string().optional(),
2193
+ ariaRequired: z.boolean().optional(),
2194
+ keyboardInteractions: z.array(z.string()).optional(),
2195
+ contrastRatio: z.number().optional(),
2196
+ focusVisible: z.boolean().optional(),
2197
+ semanticElement: z.string().optional().describe("Semantic HTML element (e.g., 'button', 'a', 'input')"),
2198
+ supportsDisabled: z.boolean().optional().describe("Whether code supports disabled/aria-disabled state"),
2199
+ supportsError: z.boolean().optional().describe("Whether code supports aria-invalid/error state"),
2200
+ // NOTE: Use array-with-length, NOT z.tuple — tuples emit JSON Schema `items: [...]`
2201
+ // (array of schemas), which Gemini's stricter Function Calling validator rejects with
2202
+ // "is not of type 'object', 'boolean'". See issue #64. A constrained array emits
2203
+ // `items: { type: 'number' }` which all major MCP clients accept.
2204
+ renderedSize: z.array(z.number()).min(2).max(2).optional().describe("Rendered size [width, height] in px"),
2205
+ }).optional().describe("Accessibility properties from code. Tip: use figma_scan_code_accessibility with mapToCodeSpec:true to auto-generate this from component HTML."),
2206
+ metadata: z.object({
2207
+ name: z.string().optional(),
2208
+ description: z.string().optional(),
2209
+ status: z.string().optional(),
2210
+ version: z.string().optional(),
2211
+ tags: z.array(z.string()).optional(),
2212
+ }).optional().describe("Component metadata from code"),
2213
+ }).describe("Structured code-side component data. Read the component source code first, then fill in the relevant sections.");
2214
+ const codeDocInfoSchema = z.object({
2215
+ props: z.array(z.object({
2216
+ name: z.string(),
2217
+ type: z.string(),
2218
+ required: z.boolean().optional(),
2219
+ defaultValue: z.string().optional(),
2220
+ description: z.string().optional(),
2221
+ })).optional().describe("Component props"),
2222
+ events: z.array(z.object({
2223
+ name: z.string(),
2224
+ payload: z.string().optional(),
2225
+ description: z.string().optional(),
2226
+ })).optional().describe("Events emitted by the component"),
2227
+ slots: z.array(z.object({
2228
+ name: z.string(),
2229
+ description: z.string().optional(),
2230
+ })).optional().describe("Named slots"),
2231
+ importStatement: z.string().optional().describe("Import statement for the component"),
2232
+ usageExamples: z.array(z.object({
2233
+ title: z.string(),
2234
+ code: z.string(),
2235
+ language: z.string().optional(),
2236
+ })).optional().describe("Usage examples"),
2237
+ changelog: z.array(z.object({
2238
+ version: z.string(),
2239
+ date: z.string(),
2240
+ changes: z.string(),
2241
+ })).optional().describe("Changelog entries"),
2242
+ filePath: z.string().optional().describe("Component file path"),
2243
+ packageName: z.string().optional().describe("Package name"),
2244
+ variantDefinition: z.string().optional().describe("CVA or variant definition code block"),
2245
+ subComponents: z.array(z.object({
2246
+ name: z.string(),
2247
+ description: z.string().optional(),
2248
+ element: z.string().optional().describe("HTML element rendered (e.g., 'div', 'span')"),
2249
+ dataSlot: z.string().optional().describe("data-slot attribute value"),
2250
+ props: z.array(z.object({
2251
+ name: z.string(),
2252
+ type: z.string(),
2253
+ required: z.boolean().optional(),
2254
+ defaultValue: z.string().optional(),
2255
+ description: z.string().optional(),
2256
+ })).optional(),
2257
+ })).optional().describe("Sub-components that compose this component (e.g., AlertTitle, AlertDescription)"),
2258
+ sourceFiles: z.array(z.object({
2259
+ path: z.string(),
2260
+ role: z.string(),
2261
+ variants: z.number().optional(),
2262
+ description: z.string().optional(),
2263
+ })).optional().describe("All source files related to this component"),
2264
+ baseComponent: z.object({
2265
+ name: z.string(),
2266
+ url: z.string().optional(),
2267
+ description: z.string().optional(),
2268
+ }).optional().describe("Base component this extends (e.g., shadcn/ui Alert)"),
2269
+ }).describe("Code-side documentation info. Read the component source code first, then fill in relevant sections. Include variantDefinition for CVA/variant code, subComponents for composable sub-parts, sourceFiles for all related files, and baseComponent for attribution.");
2270
+ // ============================================================================
2271
+ // Tool Registration
2272
+ // ============================================================================
2273
+ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, variablesCache, options, getDesktopConnector) {
2274
+ const isRemoteMode = options?.isRemoteMode ?? false;
2275
+ // -----------------------------------------------------------------------
2276
+ // Tool: figma_check_design_parity
2277
+ // -----------------------------------------------------------------------
2278
+ server.tool("figma_check_design_parity", "Compare a Figma component's design specs against code-side data to find discrepancies. Returns a parity score, categorized discrepancies, and actionable fix items for both design-side (Figma tool calls) and code-side (file edits). Read the component source code first, then pass the data in codeSpec.", {
2279
+ fileUrl: z
2280
+ .string()
2281
+ .url()
2282
+ .optional()
2283
+ .describe("Figma file URL. Uses current URL if omitted."),
2284
+ nodeId: z.string().describe("Component node ID (e.g., '695:313')"),
2285
+ codeSpec: codeSpecSchema,
2286
+ canonicalSource: z
2287
+ .enum(["design", "code"])
2288
+ .optional()
2289
+ .default("design")
2290
+ .describe("Which source is the canonical truth. Fixes will target the other side. Default: 'design'"),
2291
+ enrich: z
2292
+ .boolean()
2293
+ .optional()
2294
+ .default(true)
2295
+ .describe("Enable token coverage and enrichment analysis. Default: true"),
2296
+ }, async ({ fileUrl, nodeId, codeSpec, canonicalSource = "design", enrich = true }) => {
2297
+ try {
2298
+ const url = fileUrl || getCurrentUrl();
2299
+ if (!url) {
2300
+ throw new Error("No Figma file URL available. Pass the fileUrl parameter or ensure the Desktop Bridge plugin is open in Figma.");
2301
+ }
2302
+ const fileKey = extractFileKey(url);
2303
+ if (!fileKey) {
2304
+ throw new Error(`Invalid Figma URL: ${url}`);
2305
+ }
2306
+ logger.info({ fileKey, nodeId, canonicalSource, enrich }, "Starting design-code parity check");
2307
+ const api = await getFigmaAPI();
2308
+ // Fetch component node
2309
+ const nodesResponse = await api.getNodes(fileKey, [nodeId], { depth: 4 });
2310
+ const nodeData = nodesResponse?.nodes?.[nodeId];
2311
+ if (!nodeData?.document) {
2312
+ throw new Error(`Node ${nodeId} not found in file ${fileKey}`);
2313
+ }
2314
+ const node = nodeData.document;
2315
+ // Resolve the node to use for visual/spacing/typography comparisons.
2316
+ // COMPONENT_SET frames have container styling (purple annotation stroke, etc.)
2317
+ // that are NOT actual design specs — the real properties live on the variants.
2318
+ const nodeForVisual = resolveVisualNode(node);
2319
+ if (nodeForVisual !== node) {
2320
+ logger.info({ defaultVariant: nodeForVisual.name, type: node.type }, "Using default variant for visual comparison");
2321
+ }
2322
+ // Fetch component metadata for descriptions
2323
+ let componentMeta = null;
2324
+ let allComponentsMeta = null;
2325
+ try {
2326
+ const componentsResponse = await api.getComponents(fileKey);
2327
+ if (componentsResponse?.meta?.components) {
2328
+ allComponentsMeta = componentsResponse.meta.components;
2329
+ componentMeta = allComponentsMeta.find((c) => c.node_id === nodeId);
2330
+ }
2331
+ }
2332
+ catch {
2333
+ logger.warn("Could not fetch component metadata");
2334
+ }
2335
+ // Resolve COMPONENT_SET info (property definitions, set name)
2336
+ let setInfo = { setName: null, setNodeId: null, propertyDefinitions: {} };
2337
+ if (node.type === "COMPONENT_SET") {
2338
+ // We already have the set — read property definitions directly
2339
+ setInfo = {
2340
+ setName: node.name,
2341
+ setNodeId: nodeId,
2342
+ propertyDefinitions: node.componentPropertyDefinitions || {},
2343
+ };
2344
+ }
2345
+ else if (node.type === "COMPONENT" && isVariantName(node.name)) {
2346
+ try {
2347
+ setInfo = await resolveComponentSetInfo(api, fileKey, nodeId, componentMeta, allComponentsMeta);
2348
+ if (setInfo.setName) {
2349
+ logger.info({ setName: setInfo.setName, setNodeId: setInfo.setNodeId }, "Resolved parent component set");
2350
+ }
2351
+ }
2352
+ catch {
2353
+ logger.warn("Could not resolve parent component set");
2354
+ }
2355
+ }
2356
+ // Build a merged node for componentAPI comparison (use set's property definitions)
2357
+ const nodeForAPI = Object.keys(setInfo.propertyDefinitions).length > 0
2358
+ ? { ...node, componentPropertyDefinitions: setInfo.propertyDefinitions }
2359
+ : node;
2360
+ // Enrichment for token analysis
2361
+ let enrichedData = null;
2362
+ if (enrich) {
2363
+ try {
2364
+ const enrichmentOptions = {
2365
+ enrich: true,
2366
+ include_usage: true,
2367
+ };
2368
+ enrichedData = await enrichmentService.enrichComponent(node, fileKey, enrichmentOptions);
2369
+ }
2370
+ catch {
2371
+ logger.warn("Enrichment failed, proceeding without token data");
2372
+ }
2373
+ }
2374
+ // Cast to the structural CodeSpec interface. The Zod schema infers
2375
+ // `accessibility.renderedSize` as `number[]` (post-#64 fix uses
2376
+ // `z.array(z.number()).min(2).max(2)` for Gemini compat), but at runtime
2377
+ // the validator guarantees exactly two numbers, matching CodeSpec's
2378
+ // `[number, number]`. TypeScript can't bridge the inference gap.
2379
+ const codeSpecTyped = codeSpec;
2380
+ // Run all comparators (use nodeForVisual for design properties, nodeForAPI for component API)
2381
+ const discrepancies = [];
2382
+ compareVisual(nodeForVisual, codeSpecTyped, discrepancies);
2383
+ compareSpacing(nodeForVisual, codeSpecTyped, discrepancies);
2384
+ compareTypography(nodeForVisual, codeSpecTyped, discrepancies);
2385
+ compareTokens(enrichedData, codeSpecTyped, discrepancies);
2386
+ compareComponentAPI(nodeForAPI, codeSpecTyped, discrepancies);
2387
+ compareAccessibility(node, codeSpecTyped, discrepancies);
2388
+ compareNaming(node, codeSpecTyped, discrepancies);
2389
+ compareMetadata(node, componentMeta, codeSpecTyped, discrepancies);
2390
+ // Sort by severity
2391
+ const severityOrder = {
2392
+ critical: 0,
2393
+ major: 1,
2394
+ minor: 2,
2395
+ info: 3,
2396
+ };
2397
+ discrepancies.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
2398
+ // Calculate scores
2399
+ const counts = { critical: 0, major: 0, minor: 0, info: 0 };
2400
+ const categoryMap = {};
2401
+ for (const d of discrepancies) {
2402
+ counts[d.severity]++;
2403
+ categoryMap[d.category] = (categoryMap[d.category] || 0) + 1;
2404
+ }
2405
+ const parityScore = calculateParityScore(counts.critical, counts.major, counts.minor, counts.info);
2406
+ // Generate action items
2407
+ const actionItems = generateActionItems(discrepancies, nodeId, canonicalSource, codeSpec.filePath);
2408
+ const resolvedName = resolveComponentName(node, setInfo.setName, codeSpec.metadata?.name || codeSpec.filePath?.split("/").pop()?.replace(/\.\w+$/, ""));
2409
+ const result = {
2410
+ summary: {
2411
+ totalDiscrepancies: discrepancies.length,
2412
+ parityScore,
2413
+ byCritical: counts.critical,
2414
+ byMajor: counts.major,
2415
+ byMinor: counts.minor,
2416
+ byInfo: counts.info,
2417
+ categories: categoryMap,
2418
+ },
2419
+ discrepancies,
2420
+ actionItems,
2421
+ ai_instruction: buildParityInstruction(resolvedName, parityScore, counts, canonicalSource, discrepancies.length),
2422
+ designData: {
2423
+ name: node.name,
2424
+ resolvedName,
2425
+ type: node.type,
2426
+ isComponentSet: node.type === "COMPONENT_SET",
2427
+ defaultVariantName: node.type === "COMPONENT_SET" ? nodeForVisual.name : undefined,
2428
+ componentSetName: setInfo.setName,
2429
+ componentSetNodeId: setInfo.setNodeId,
2430
+ fills: nodeForVisual.fills,
2431
+ strokes: nodeForVisual.strokes,
2432
+ cornerRadius: nodeForVisual.cornerRadius,
2433
+ opacity: nodeForVisual.opacity,
2434
+ spacing: extractSpacingProperties(nodeForVisual),
2435
+ componentProperties: nodeForAPI.componentPropertyDefinitions
2436
+ ? Object.keys(nodeForAPI.componentPropertyDefinitions)
2437
+ : [],
2438
+ tokenCoverage: enrichedData?.token_coverage,
2439
+ },
2440
+ codeData: codeSpecTyped,
2441
+ };
2442
+ return {
2443
+ content: [{ type: "text", text: JSON.stringify(result) }],
2444
+ };
2445
+ }
2446
+ catch (error) {
2447
+ const message = error instanceof Error ? error.message : String(error);
2448
+ logger.error({ error: message, nodeId }, "Design parity check failed");
2449
+ return {
2450
+ content: [
2451
+ {
2452
+ type: "text",
2453
+ text: JSON.stringify({
2454
+ error: message,
2455
+ ai_instruction: `Design parity check failed: ${message}. Verify the nodeId is correct and the Figma file is accessible.`,
2456
+ }),
2457
+ },
2458
+ ],
2459
+ isError: true,
2460
+ };
2461
+ }
2462
+ });
2463
+ // -----------------------------------------------------------------------
2464
+ // Tool: figma_generate_component_doc
2465
+ // -----------------------------------------------------------------------
2466
+ server.tool("figma_generate_component_doc", "Generate AI-complete component documentation from a Figma component. Produces structured markdown with anatomy, per-variant color tokens, typography, content guidelines (parsed from Figma description), design annotations (animation timings, interaction specs, accessibility notes from Dev Mode), icon mapping, spacing tokens, and design-code parity analysis. Merges Figma design data with optional code-side info (CVA definitions, sub-component APIs, source files). Output works with any docs platform. For richest output, read the component source code first and pass codeInfo.", {
2467
+ fileUrl: z
2468
+ .string()
2469
+ .url()
2470
+ .optional()
2471
+ .describe("Figma file URL. Uses current URL if omitted."),
2472
+ nodeId: z.string().describe("Component node ID (e.g., '695:313')"),
2473
+ codeInfo: codeDocInfoSchema.optional(),
2474
+ sections: z.object({
2475
+ overview: z.boolean().optional().default(true),
2476
+ anatomy: z.boolean().optional().default(true),
2477
+ statesAndVariants: z.boolean().optional().default(true),
2478
+ visualSpecs: z.boolean().optional().default(true),
2479
+ typography: z.boolean().optional().default(true),
2480
+ contentGuidelines: z.boolean().optional().default(true),
2481
+ behavior: z.boolean().optional().default(false),
2482
+ implementation: z.boolean().optional().default(true),
2483
+ accessibility: z.boolean().optional().default(true),
2484
+ designAnnotations: z.boolean().optional().default(true),
2485
+ relatedComponents: z.boolean().optional().default(false),
2486
+ changelog: z.boolean().optional().default(true),
2487
+ parity: z.boolean().optional().default(true),
2488
+ }).optional().describe("Toggle which sections to include"),
2489
+ outputPath: z.string().optional().describe("Suggested output file path"),
2490
+ systemName: z.string().optional().describe("Design system name for headers"),
2491
+ enrich: z.boolean().optional().default(true).describe("Enable enrichment for token data"),
2492
+ includeFrontmatter: z.boolean().optional().default(true).describe("Include YAML frontmatter metadata"),
2493
+ }, async ({ fileUrl, nodeId, codeInfo, sections, outputPath, systemName, enrich = true, includeFrontmatter = true, }) => {
2494
+ try {
2495
+ const url = fileUrl || getCurrentUrl();
2496
+ if (!url) {
2497
+ throw new Error("No Figma file URL available. Pass the fileUrl parameter or ensure the Desktop Bridge plugin is open in Figma.");
2498
+ }
2499
+ const fileKey = extractFileKey(url);
2500
+ if (!fileKey) {
2501
+ throw new Error(`Invalid Figma URL: ${url}`);
2502
+ }
2503
+ logger.info({ fileKey, nodeId, enrich }, "Generating component documentation");
2504
+ const api = await getFigmaAPI();
2505
+ // Fetch component node with deeper depth for anatomy & per-variant data
2506
+ const nodesResponse = await api.getNodes(fileKey, [nodeId], { depth: 4 });
2507
+ const nodeData = nodesResponse?.nodes?.[nodeId];
2508
+ if (!nodeData?.document) {
2509
+ throw new Error(`Node ${nodeId} not found in file ${fileKey}`);
2510
+ }
2511
+ const node = nodeData.document;
2512
+ // Resolve visual node (default variant for COMPONENT_SET, node itself otherwise)
2513
+ const nodeForVisual = resolveVisualNode(node);
2514
+ if (nodeForVisual !== node) {
2515
+ logger.info({ defaultVariant: nodeForVisual.name, type: node.type }, "Using default variant for visual specs in docs");
2516
+ }
2517
+ // Fetch component metadata
2518
+ let componentMeta = null;
2519
+ let allComponentsMeta = null;
2520
+ try {
2521
+ const componentsResponse = await api.getComponents(fileKey);
2522
+ if (componentsResponse?.meta?.components) {
2523
+ allComponentsMeta = componentsResponse.meta.components;
2524
+ componentMeta = allComponentsMeta.find((c) => c.node_id === nodeId);
2525
+ }
2526
+ }
2527
+ catch {
2528
+ logger.warn("Could not fetch component metadata");
2529
+ }
2530
+ // Resolve COMPONENT_SET info (property definitions, set name)
2531
+ let setInfo = { setName: null, setNodeId: null, propertyDefinitions: {} };
2532
+ if (node.type === "COMPONENT_SET") {
2533
+ setInfo = {
2534
+ setName: node.name,
2535
+ setNodeId: nodeId,
2536
+ propertyDefinitions: node.componentPropertyDefinitions || {},
2537
+ };
2538
+ }
2539
+ else if (node.type === "COMPONENT" && isVariantName(node.name)) {
2540
+ try {
2541
+ setInfo = await resolveComponentSetInfo(api, fileKey, nodeId, componentMeta, allComponentsMeta);
2542
+ if (setInfo.setName) {
2543
+ logger.info({ setName: setInfo.setName, setNodeId: setInfo.setNodeId }, "Resolved parent component set for docs");
2544
+ }
2545
+ }
2546
+ catch {
2547
+ logger.warn("Could not resolve parent component set");
2548
+ }
2549
+ }
2550
+ // Use set's property definitions for variants section if available
2551
+ const nodeForVariants = Object.keys(setInfo.propertyDefinitions).length > 0
2552
+ ? { ...node, componentPropertyDefinitions: setInfo.propertyDefinitions }
2553
+ : node;
2554
+ // Enrichment
2555
+ let enrichedData = null;
2556
+ if (enrich) {
2557
+ try {
2558
+ const enrichmentOptions = {
2559
+ enrich: true,
2560
+ include_usage: true,
2561
+ };
2562
+ enrichedData = await enrichmentService.enrichComponent(node, fileKey, enrichmentOptions);
2563
+ }
2564
+ catch {
2565
+ logger.warn("Enrichment failed, proceeding without token data");
2566
+ }
2567
+ }
2568
+ // Build variable name lookup for per-variant color collection
2569
+ const varNameMap = new Map();
2570
+ if (enrichedData?.variables_used) {
2571
+ for (const v of enrichedData.variables_used) {
2572
+ varNameMap.set(v.id, v.name);
2573
+ }
2574
+ }
2575
+ // Collect per-variant color/icon data
2576
+ const variantData = collectAllVariantData(node, varNameMap);
2577
+ // Resolve clean component name (prefer set name over variant name)
2578
+ const componentName = resolveComponentName(node, setInfo.setName, codeInfo?.filePath?.split("/").pop()?.replace(/\.\w+$/, ""));
2579
+ // Prefer markdown description (has headers/bold markers for parsing) over plain text
2580
+ // REST API getNodes() often returns empty description for COMPONENT_SET nodes,
2581
+ // so fall back to Desktop Bridge plugin API which has the reliable description.
2582
+ let description = node.descriptionMarkdown || node.description || componentMeta?.description || "";
2583
+ if (getDesktopConnector) {
2584
+ try {
2585
+ const connector = await getDesktopConnector();
2586
+ const bridgeResult = await connector.getComponentFromPluginUI(nodeId);
2587
+ if (bridgeResult.success && bridgeResult.component) {
2588
+ // Fetch description from bridge if REST API returned empty
2589
+ if (!description) {
2590
+ description = bridgeResult.component.descriptionMarkdown || bridgeResult.component.description || "";
2591
+ if (description) {
2592
+ logger.info("Fetched description via Desktop Bridge (REST API returned empty)");
2593
+ }
2594
+ }
2595
+ // Always fetch annotations from bridge (REST API never has them)
2596
+ if (bridgeResult.component.annotations && bridgeResult.component.annotations.length > 0) {
2597
+ node.annotations = bridgeResult.component.annotations;
2598
+ logger.info({ count: node.annotations.length }, "Fetched annotations via Desktop Bridge for documentation");
2599
+ }
2600
+ }
2601
+ }
2602
+ catch {
2603
+ logger.warn("Desktop Bridge fetch failed, proceeding without bridge-sourced data");
2604
+ }
2605
+ }
2606
+ // Strip any existing query (e.g. the connected file's ?node-id=<page>) before
2607
+ // appending the target node, otherwise the URL ends up with a doubled ?node-id=.
2608
+ const fileUrl_ = `${url.split("?")[0]}?node-id=${nodeId.replace(":", "-")}`;
2609
+ // Parse the component description for structured content
2610
+ const parsedDesc = parseComponentDescription(description);
2611
+ // Determine canonical source
2612
+ const hasCodeInfo = codeInfo !== undefined && codeInfo !== null;
2613
+ const hasFigmaData = node.type === "COMPONENT" || node.type === "COMPONENT_SET";
2614
+ const canonicalSource = hasFigmaData && hasCodeInfo ? "reconciled"
2615
+ : hasCodeInfo ? "code"
2616
+ : "figma";
2617
+ // Resolve sections with defaults
2618
+ const s = {
2619
+ overview: true,
2620
+ anatomy: true,
2621
+ statesAndVariants: true,
2622
+ visualSpecs: true,
2623
+ typography: true,
2624
+ contentGuidelines: true,
2625
+ behavior: false,
2626
+ implementation: true,
2627
+ accessibility: true,
2628
+ relatedComponents: false,
2629
+ changelog: true,
2630
+ parity: true,
2631
+ ...sections,
2632
+ };
2633
+ // Build markdown
2634
+ const parts = [];
2635
+ const includedSections = [];
2636
+ if (includeFrontmatter) {
2637
+ const atomicLevel = await detectAtomicLevel(api, fileKey, nodeId, setInfo.setNodeId, componentMeta, allComponentsMeta);
2638
+ parts.push(generateFrontmatter(componentName, description, node, componentMeta, fileUrl_, codeInfo, canonicalSource, atomicLevel));
2639
+ parts.push("");
2640
+ }
2641
+ if (s.overview) {
2642
+ parts.push(generateOverviewSection(componentName, description, fileUrl_, parsedDesc, codeInfo));
2643
+ includedSections.push("overview");
2644
+ }
2645
+ if (s.anatomy) {
2646
+ const anatomySection = generateAnatomySection(node);
2647
+ if (anatomySection.trim()) {
2648
+ parts.push(anatomySection);
2649
+ includedSections.push("anatomy");
2650
+ }
2651
+ }
2652
+ if (s.statesAndVariants) {
2653
+ const variantsSection = generateStatesAndVariantsSection(nodeForVariants, variantData);
2654
+ if (variantsSection) {
2655
+ parts.push(variantsSection);
2656
+ includedSections.push("statesAndVariants");
2657
+ }
2658
+ }
2659
+ if (s.visualSpecs) {
2660
+ parts.push(generateVisualSpecsSection(nodeForVisual, enrichedData, variantData));
2661
+ includedSections.push("visualSpecs");
2662
+ }
2663
+ if (s.typography) {
2664
+ const typoSection = generateTypographySection(node);
2665
+ if (typoSection) {
2666
+ parts.push(typoSection);
2667
+ includedSections.push("typography");
2668
+ }
2669
+ }
2670
+ if (s.contentGuidelines) {
2671
+ const contentSection = generateContentGuidelinesSection(parsedDesc);
2672
+ if (contentSection) {
2673
+ parts.push(contentSection);
2674
+ includedSections.push("contentGuidelines");
2675
+ }
2676
+ }
2677
+ if (s.implementation && codeInfo) {
2678
+ parts.push(generateImplementationSection(codeInfo));
2679
+ includedSections.push("implementation");
2680
+ }
2681
+ if (s.accessibility) {
2682
+ parts.push(generateAccessibilitySection(node, parsedDesc, codeInfo));
2683
+ includedSections.push("accessibility");
2684
+ }
2685
+ if (s.designAnnotations !== false) {
2686
+ const annotationsSection = generateDesignAnnotationsSection(node);
2687
+ parts.push(annotationsSection);
2688
+ includedSections.push("designAnnotations");
2689
+ }
2690
+ if (s.parity && hasCodeInfo && hasFigmaData && codeInfo) {
2691
+ const paritySection = generateParitySection(node, codeInfo);
2692
+ if (paritySection) {
2693
+ parts.push(paritySection);
2694
+ includedSections.push("parity");
2695
+ }
2696
+ }
2697
+ if (s.changelog && codeInfo?.changelog) {
2698
+ parts.push(generateChangelogSection(codeInfo));
2699
+ includedSections.push("changelog");
2700
+ }
2701
+ const markdown = parts.join("\n");
2702
+ const sanitizedName = sanitizeComponentName(componentName);
2703
+ const suggestedPath = outputPath || `docs/components/${sanitizedName}.md`;
2704
+ // Build enhanced AI instruction
2705
+ const aiInstParts = [
2706
+ `Documentation generated for ${componentName} component (canonical source: ${canonicalSource}).`,
2707
+ `Ask the user where they'd like to save this file. Suggested path: ${suggestedPath}`,
2708
+ ];
2709
+ if (!hasCodeInfo) {
2710
+ aiInstParts.push("", "To enhance this documentation, read the component's source code and call this tool again with codeInfo including:", "- filePath: path to the main component file", "- importStatement: how to import the component", "- props: component API (name, type, required, defaultValue, description)", "- variantDefinition: CVA or variant definition code block", "- subComponents: sub-components with their props (e.g., AlertTitle, AlertDescription)", "- sourceFiles: all related source files with roles", "- baseComponent: base component attribution (e.g., shadcn/ui)", "- usageExamples: code examples for each variant/use case");
2711
+ }
2712
+ const result = {
2713
+ componentName,
2714
+ figmaNodeId: nodeId,
2715
+ fileKey,
2716
+ timestamp: new Date().toISOString(),
2717
+ markdown,
2718
+ includedSections,
2719
+ canonicalSource,
2720
+ dataSourceSummary: {
2721
+ figmaEnriched: enrichedData !== null,
2722
+ hasCodeInfo,
2723
+ variablesIncluded: enrichedData?.variables_used !== undefined,
2724
+ stylesIncluded: enrichedData?.styles_used !== undefined,
2725
+ },
2726
+ suggestedOutputPath: suggestedPath,
2727
+ ai_instruction: aiInstParts.join("\n"),
2728
+ };
2729
+ return {
2730
+ content: [{ type: "text", text: JSON.stringify(result) }],
2731
+ };
2732
+ }
2733
+ catch (error) {
2734
+ const message = error instanceof Error ? error.message : String(error);
2735
+ logger.error({ error: message, nodeId }, "Documentation generation failed");
2736
+ return {
2737
+ content: [
2738
+ {
2739
+ type: "text",
2740
+ text: JSON.stringify({
2741
+ error: message,
2742
+ ai_instruction: `Documentation generation failed: ${message}. Verify the nodeId is correct and the Figma file is accessible.`,
2743
+ }),
2744
+ },
2745
+ ],
2746
+ isError: true,
2747
+ };
2748
+ }
2749
+ });
2750
+ }
2751
+ //# sourceMappingURL=design-code-tools.js.map