@sarjallab09/figma-intelligence 1.0.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 (286) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +327 -0
  3. package/bin/cli.js +859 -0
  4. package/design-bridge/.env.example +5 -0
  5. package/design-bridge/bridge.js +196 -0
  6. package/design-bridge/lib/assets.js +367 -0
  7. package/design-bridge/lib/prompt.js +85 -0
  8. package/design-bridge/lib/server.js +66 -0
  9. package/design-bridge/lib/stitch.js +37 -0
  10. package/design-bridge/lib/tokens.js +82 -0
  11. package/design-bridge/package-lock.json +579 -0
  12. package/design-bridge/package.json +19 -0
  13. package/figma-bridge-plugin/README.md +97 -0
  14. package/figma-bridge-plugin/anthropic-chat-runner.js +192 -0
  15. package/figma-bridge-plugin/bridge-relay.js +2363 -0
  16. package/figma-bridge-plugin/chat-runner.js +459 -0
  17. package/figma-bridge-plugin/code.js +1528 -0
  18. package/figma-bridge-plugin/codex-runner.js +505 -0
  19. package/figma-bridge-plugin/component-schemas.js +110 -0
  20. package/figma-bridge-plugin/content-context.js +869 -0
  21. package/figma-bridge-plugin/create-button.js +216 -0
  22. package/figma-bridge-plugin/gemini-cli-runner.js +291 -0
  23. package/figma-bridge-plugin/gemini-runner.js +187 -0
  24. package/figma-bridge-plugin/html-to-figma.js +927 -0
  25. package/figma-bridge-plugin/knowledge-hub/.gitkeep +0 -0
  26. package/figma-bridge-plugin/knowledge-hub/uspec-references/anatomy-spec.md +159 -0
  27. package/figma-bridge-plugin/knowledge-hub/uspec-references/api-spec.md +162 -0
  28. package/figma-bridge-plugin/knowledge-hub/uspec-references/color-spec.md +148 -0
  29. package/figma-bridge-plugin/knowledge-hub/uspec-references/full-spec-template.md +314 -0
  30. package/figma-bridge-plugin/knowledge-hub/uspec-references/property-spec.md +175 -0
  31. package/figma-bridge-plugin/knowledge-hub/uspec-references/screen-reader-spec.md +180 -0
  32. package/figma-bridge-plugin/knowledge-hub/uspec-references/structure-spec.md +165 -0
  33. package/figma-bridge-plugin/manifest.json +21 -0
  34. package/figma-bridge-plugin/package-lock.json +1936 -0
  35. package/figma-bridge-plugin/package.json +20 -0
  36. package/figma-bridge-plugin/perplexity-runner.js +188 -0
  37. package/figma-bridge-plugin/references/SKILL.md +178 -0
  38. package/figma-bridge-plugin/references/anatomy-spec.md +159 -0
  39. package/figma-bridge-plugin/references/api-spec.md +162 -0
  40. package/figma-bridge-plugin/references/color-spec.md +148 -0
  41. package/figma-bridge-plugin/references/full-spec-template.md +314 -0
  42. package/figma-bridge-plugin/references/property-spec.md +175 -0
  43. package/figma-bridge-plugin/references/screen-reader-spec.md +180 -0
  44. package/figma-bridge-plugin/references/structure-spec.md +165 -0
  45. package/figma-bridge-plugin/shared-prompt-config.js +604 -0
  46. package/figma-bridge-plugin/spec-helpers/build-table.js +269 -0
  47. package/figma-bridge-plugin/spec-helpers/classify-elements.js +189 -0
  48. package/figma-bridge-plugin/spec-helpers/index.js +35 -0
  49. package/figma-bridge-plugin/spec-helpers/parse-figma-link.js +49 -0
  50. package/figma-bridge-plugin/spec-helpers/position-markers.js +158 -0
  51. package/figma-bridge-plugin/stitch-auth.js +322 -0
  52. package/figma-bridge-plugin/stitch-runner.js +1427 -0
  53. package/figma-bridge-plugin/token-resolver.js +107 -0
  54. package/figma-bridge-plugin/ui.html +4467 -0
  55. package/figma-intelligence-layer/.env.example +39 -0
  56. package/figma-intelligence-layer/docs/local-image-generation.md +60 -0
  57. package/figma-intelligence-layer/examples/comfyui-workflow-template.example.json +101 -0
  58. package/figma-intelligence-layer/jest.config.js +14 -0
  59. package/figma-intelligence-layer/mcp-config.json +19 -0
  60. package/figma-intelligence-layer/package-lock.json +5892 -0
  61. package/figma-intelligence-layer/package.json +48 -0
  62. package/figma-intelligence-layer/scripts/setup-comfyui-local.sh +67 -0
  63. package/figma-intelligence-layer/scripts/start-comfyui.sh +33 -0
  64. package/figma-intelligence-layer/src/index.ts +2233 -0
  65. package/figma-intelligence-layer/src/shared/auto-layout-validator.ts +404 -0
  66. package/figma-intelligence-layer/src/shared/cache.ts +187 -0
  67. package/figma-intelligence-layer/src/shared/color-operations.ts +533 -0
  68. package/figma-intelligence-layer/src/shared/color-utils.ts +138 -0
  69. package/figma-intelligence-layer/src/shared/component-script-builder.ts +413 -0
  70. package/figma-intelligence-layer/src/shared/component-templates.ts +2767 -0
  71. package/figma-intelligence-layer/src/shared/concept-taxonomy.ts +694 -0
  72. package/figma-intelligence-layer/src/shared/decision-log.ts +128 -0
  73. package/figma-intelligence-layer/src/shared/design-system-context.ts +568 -0
  74. package/figma-intelligence-layer/src/shared/design-system-intelligence.ts +131 -0
  75. package/figma-intelligence-layer/src/shared/design-system-matcher.ts +184 -0
  76. package/figma-intelligence-layer/src/shared/design-system-normalizers.ts +196 -0
  77. package/figma-intelligence-layer/src/shared/design-system-tokens.ts +295 -0
  78. package/figma-intelligence-layer/src/shared/dtcg-validator.ts +530 -0
  79. package/figma-intelligence-layer/src/shared/enrichment-pipeline.ts +671 -0
  80. package/figma-intelligence-layer/src/shared/figma-bridge.ts +1408 -0
  81. package/figma-intelligence-layer/src/shared/font-config.ts +126 -0
  82. package/figma-intelligence-layer/src/shared/icon-catalog.ts +360 -0
  83. package/figma-intelligence-layer/src/shared/icon-fetch.ts +80 -0
  84. package/figma-intelligence-layer/src/shared/prototype-script-builder.ts +162 -0
  85. package/figma-intelligence-layer/src/shared/response-compression.ts +440 -0
  86. package/figma-intelligence-layer/src/shared/semantic-token-catalog.ts +324 -0
  87. package/figma-intelligence-layer/src/shared/token-binder.ts +505 -0
  88. package/figma-intelligence-layer/src/shared/token-math.ts +427 -0
  89. package/figma-intelligence-layer/src/shared/token-naming.ts +468 -0
  90. package/figma-intelligence-layer/src/shared/token-utils.ts +420 -0
  91. package/figma-intelligence-layer/src/shared/types.ts +346 -0
  92. package/figma-intelligence-layer/src/shared/typography-presets.ts +94 -0
  93. package/figma-intelligence-layer/src/shared/unsplash.ts +165 -0
  94. package/figma-intelligence-layer/src/shared/vision-client.ts +607 -0
  95. package/figma-intelligence-layer/src/shared/vision-provider-anthropic.ts +334 -0
  96. package/figma-intelligence-layer/src/shared/vision-provider-openai.ts +446 -0
  97. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotate-handler.ts +782 -0
  98. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotate-renderer.ts +496 -0
  99. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotation-kit.ts +230 -0
  100. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/colorblind-sim.ts +66 -0
  101. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/index.ts +810 -0
  102. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-analyzer.ts +1191 -0
  103. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-figma-page.ts +1346 -0
  104. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-handler.ts +148 -0
  105. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/vpat-figma-page.ts +499 -0
  106. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/vpat-report.ts +910 -0
  107. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/wcag-checker.ts +989 -0
  108. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/wcag-criteria.ts +1160 -0
  109. package/figma-intelligence-layer/src/tools/phase1-vision/design-from-ref/index.ts +424 -0
  110. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/component-recognizer.ts +38 -0
  111. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/ds-matcher.ts +111 -0
  112. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/font-matcher.ts +114 -0
  113. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/icon-resolver.ts +103 -0
  114. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/index.ts +1060 -0
  115. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/layout-segmenter.ts +18 -0
  116. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/token-inferencer.ts +39 -0
  117. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/vision-pipeline.ts +58 -0
  118. package/figma-intelligence-layer/src/tools/phase1-vision/sketch-to-design/index.ts +298 -0
  119. package/figma-intelligence-layer/src/tools/phase1-vision/visual-audit/index.ts +197 -0
  120. package/figma-intelligence-layer/src/tools/phase2-accuracy/component-audit/index.ts +494 -0
  121. package/figma-intelligence-layer/src/tools/phase2-accuracy/intent-translator/index.ts +356 -0
  122. package/figma-intelligence-layer/src/tools/phase2-accuracy/layout-intelligence/container-patterns.ts +123 -0
  123. package/figma-intelligence-layer/src/tools/phase2-accuracy/layout-intelligence/index.ts +663 -0
  124. package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/built-in-rules.yaml +56 -0
  125. package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/index.ts +614 -0
  126. package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/rule-engine.ts +113 -0
  127. package/figma-intelligence-layer/src/tools/phase2-accuracy/theme-generator/color-theory.ts +178 -0
  128. package/figma-intelligence-layer/src/tools/phase2-accuracy/theme-generator/index.ts +470 -0
  129. package/figma-intelligence-layer/src/tools/phase2-accuracy/variant-expander/index.ts +429 -0
  130. package/figma-intelligence-layer/src/tools/phase2-accuracy/variant-expander/token-override-maps.ts +226 -0
  131. package/figma-intelligence-layer/src/tools/phase3-generation/ai-image-insert/index.ts +535 -0
  132. package/figma-intelligence-layer/src/tools/phase3-generation/component-archaeologist/index.ts +660 -0
  133. package/figma-intelligence-layer/src/tools/phase3-generation/component-archaeologist/pattern-fingerprints.ts +209 -0
  134. package/figma-intelligence-layer/src/tools/phase3-generation/composition-builder/index.ts +540 -0
  135. package/figma-intelligence-layer/src/tools/phase3-generation/figma-animated-build.ts +391 -0
  136. package/figma-intelligence-layer/src/tools/phase3-generation/page-architect/index.ts +2019 -0
  137. package/figma-intelligence-layer/src/tools/phase3-generation/page-architect/screen-templates.ts +131 -0
  138. package/figma-intelligence-layer/src/tools/phase3-generation/prototype-map/index.ts +381 -0
  139. package/figma-intelligence-layer/src/tools/phase3-generation/prototype-wire/index.ts +565 -0
  140. package/figma-intelligence-layer/src/tools/phase3-generation/swarm-build/index.ts +764 -0
  141. package/figma-intelligence-layer/src/tools/phase3-generation/system-drift/index.ts +535 -0
  142. package/figma-intelligence-layer/src/tools/phase3-generation/unsplash-search/index.ts +84 -0
  143. package/figma-intelligence-layer/src/tools/phase3-generation/url-to-frame/index.ts +401 -0
  144. package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/css-animations.ts +68 -0
  145. package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/framer-motion.ts +78 -0
  146. package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/swift-animations.ts +93 -0
  147. package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/index.ts +596 -0
  148. package/figma-intelligence-layer/src/tools/phase4-sync/ci-check/index.ts +462 -0
  149. package/figma-intelligence-layer/src/tools/phase4-sync/export-tokens/index.ts +1470 -0
  150. package/figma-intelligence-layer/src/tools/phase4-sync/generate-component-code/index.ts +829 -0
  151. package/figma-intelligence-layer/src/tools/phase4-sync/handoff-spec/index.ts +702 -0
  152. package/figma-intelligence-layer/src/tools/phase4-sync/icon-library-sync/index.ts +483 -0
  153. package/figma-intelligence-layer/src/tools/phase4-sync/sync-from-code/index.ts +501 -0
  154. package/figma-intelligence-layer/src/tools/phase4-sync/sync-from-code/storybook-parser.ts +106 -0
  155. package/figma-intelligence-layer/src/tools/phase4-sync/watch-docs/index.ts +676 -0
  156. package/figma-intelligence-layer/src/tools/phase4-sync/webhook-listener/index.ts +560 -0
  157. package/figma-intelligence-layer/src/tools/phase5-governance/apg-doc/index.ts +1043 -0
  158. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/component-detection.ts +620 -0
  159. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/anatomy.ts +331 -0
  160. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/color-tokens.ts +77 -0
  161. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/properties.ts +54 -0
  162. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/snapshot.ts +287 -0
  163. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/spacing.ts +71 -0
  164. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/states.ts +43 -0
  165. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/typography.ts +71 -0
  166. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/index.ts +221 -0
  167. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/_default.ts +166 -0
  168. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/accordion.ts +232 -0
  169. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/alert.ts +234 -0
  170. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/avatar-group.ts +270 -0
  171. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/avatar.ts +249 -0
  172. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/badge.ts +231 -0
  173. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/banner.ts +293 -0
  174. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/breadcrumb.ts +240 -0
  175. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/button.ts +243 -0
  176. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/calendar.ts +307 -0
  177. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/card.ts +143 -0
  178. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/checkbox.ts +227 -0
  179. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/chip.ts +233 -0
  180. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/combobox.ts +282 -0
  181. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/datepicker.ts +276 -0
  182. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/divider.ts +223 -0
  183. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/drawer.ts +255 -0
  184. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/dropdown-menu.ts +289 -0
  185. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/empty-state.ts +261 -0
  186. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/file-uploader.ts +290 -0
  187. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/form.ts +265 -0
  188. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/grid.ts +238 -0
  189. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/icon.ts +255 -0
  190. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/index.ts +128 -0
  191. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/inline-edit.ts +286 -0
  192. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/inline-message.ts +255 -0
  193. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/input.ts +330 -0
  194. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/link.ts +247 -0
  195. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/list.ts +250 -0
  196. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/menu.ts +247 -0
  197. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/modal.ts +144 -0
  198. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/navbar.ts +264 -0
  199. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/navigation.ts +251 -0
  200. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/number-input.ts +261 -0
  201. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/pagination.ts +248 -0
  202. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/popover.ts +270 -0
  203. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/progress.ts +251 -0
  204. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/radio.ts +142 -0
  205. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/range-slider.ts +282 -0
  206. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/rating.ts +250 -0
  207. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/search.ts +258 -0
  208. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/segmented-control.ts +265 -0
  209. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/select.ts +319 -0
  210. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/skeleton.ts +256 -0
  211. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/slider.ts +232 -0
  212. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/spinner.ts +239 -0
  213. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/status-dot.ts +252 -0
  214. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/stepper.ts +270 -0
  215. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/table.ts +244 -0
  216. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tabs.ts +143 -0
  217. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tag.ts +243 -0
  218. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/textarea.ts +259 -0
  219. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/time-picker.ts +293 -0
  220. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toast.ts +144 -0
  221. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toggle.ts +289 -0
  222. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toolbar.ts +267 -0
  223. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tooltip.ts +232 -0
  224. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/treeview.ts +257 -0
  225. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/typography.ts +319 -0
  226. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/legacy-compat.ts +121 -0
  227. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/anatomy-diagram.ts +430 -0
  228. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/figma-page.ts +312 -0
  229. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/json.ts +129 -0
  230. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/markdown.ts +78 -0
  231. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/visual-doc.ts +2333 -0
  232. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/accessibility.ts +100 -0
  233. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/anatomy.ts +32 -0
  234. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/color-tokens.ts +59 -0
  235. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/content-guidance.ts +18 -0
  236. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/design-tokens.ts +53 -0
  237. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/interaction-rules.ts +19 -0
  238. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/overview.ts +91 -0
  239. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/properties-api.ts +71 -0
  240. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/qa-criteria.ts +19 -0
  241. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/related-components.ts +110 -0
  242. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/responsive.ts +19 -0
  243. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/size-specs.ts +67 -0
  244. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/spacing-structure.ts +58 -0
  245. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/state-specs.ts +79 -0
  246. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/states.ts +50 -0
  247. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/type-hierarchy.ts +33 -0
  248. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/typography.ts +55 -0
  249. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/usage-guidelines.ts +73 -0
  250. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/variants.ts +81 -0
  251. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/types.ts +409 -0
  252. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/index.ts +198 -0
  253. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/renderer.ts +701 -0
  254. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/types.ts +88 -0
  255. package/figma-intelligence-layer/src/tools/phase5-governance/decision-log/index.ts +135 -0
  256. package/figma-intelligence-layer/src/tools/phase5-governance/design-decision-log/index.ts +491 -0
  257. package/figma-intelligence-layer/src/tools/phase5-governance/ds-primitives/index.ts +416 -0
  258. package/figma-intelligence-layer/src/tools/phase5-governance/ds-scaffolder/index.ts +722 -0
  259. package/figma-intelligence-layer/src/tools/phase5-governance/ds-variables/index.ts +449 -0
  260. package/figma-intelligence-layer/src/tools/phase5-governance/health-report/index.ts +393 -0
  261. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/index.ts +406 -0
  262. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/figma-page.ts +292 -0
  263. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/json.ts +24 -0
  264. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/markdown.ts +172 -0
  265. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/naming-guide.ts +409 -0
  266. package/figma-intelligence-layer/src/tools/phase5-governance/token-analytics/index.ts +594 -0
  267. package/figma-intelligence-layer/src/tools/phase5-governance/token-docs/index.ts +710 -0
  268. package/figma-intelligence-layer/src/tools/phase5-governance/token-migrate/index.ts +458 -0
  269. package/figma-intelligence-layer/src/tools/phase5-governance/token-naming/index.ts +134 -0
  270. package/figma-intelligence-layer/tests/apg-doc.test.ts +101 -0
  271. package/figma-intelligence-layer/tests/design-system-context.test.ts +152 -0
  272. package/figma-intelligence-layer/tests/design-system-matcher.test.ts +144 -0
  273. package/figma-intelligence-layer/tests/figma-bridge.test.ts +83 -0
  274. package/figma-intelligence-layer/tests/generate-image-and-insert.test.ts +56 -0
  275. package/figma-intelligence-layer/tests/screen-cloner-regression.test.ts +69 -0
  276. package/figma-intelligence-layer/tests/smoke.test.ts +174 -0
  277. package/figma-intelligence-layer/tests/spec-generator.test.ts +127 -0
  278. package/figma-intelligence-layer/tests/token-migrate.test.ts +21 -0
  279. package/figma-intelligence-layer/tests/token-naming.test.ts +30 -0
  280. package/figma-intelligence-layer/tsconfig.json +19 -0
  281. package/package.json +35 -0
  282. package/scripts/clean-existing-chunks.js +179 -0
  283. package/scripts/connect-ai-tool.js +490 -0
  284. package/scripts/convert-hub-pdfs.js +425 -0
  285. package/scripts/figma-mcp-status.js +349 -0
  286. package/scripts/register-codex-mcp.js +96 -0
@@ -0,0 +1,2333 @@
1
+ /**
2
+ * Visual Doc renderer — Enterprise exhibit-based component spec as a Figma page.
3
+ *
4
+ * Produces Zeroheight/Specify-level spec pages with:
5
+ * - 1343px wide white card sections with 48px gaps
6
+ * - Exhibit pattern: gray artwork (350×248) + content metadata
7
+ * - Orange (#C54600) anatomy markers with type icons
8
+ * - Green/orange/blue spacing overlays
9
+ * - Per-variant live instance exhibits grouped by axis
10
+ * - DS-aware font + palette integration via font-config / token-binder
11
+ *
12
+ * IMPORTANT Figma Plugin API pattern:
13
+ * layoutSizingHorizontal = "FILL" only works AFTER the node is appended
14
+ * to an auto-layout parent. Always: appendChild() first, then set sizing.
15
+ */
16
+ import { getBridge } from "../../../../shared/figma-bridge.js";
17
+ import type { ComponentSpec, SpecSectionContent, ClassifiedElement } from "../types.js";
18
+ import { computeMarkerPositions } from "./anatomy-diagram.js";
19
+
20
+ // ── Design Tokens ─────────────────────────────────────────────────────────
21
+
22
+ const PAGE_W = 1343;
23
+ const PAD = 64;
24
+ const SECTION_GAP = 48;
25
+ const CARD_PAD = 64;
26
+ const CARD_ITEM_GAP = 64;
27
+ const CONTENT_W = PAGE_W - PAD * 2; // 1215
28
+
29
+ // Exhibit artwork dimensions
30
+ const ARTWORK_W = 350;
31
+ const ARTWORK_H = 160;
32
+ const EXHIBIT_GAP = 64;
33
+
34
+ const C = {
35
+ textMain: "{r:0,g:0,b:0}", // #000000
36
+ textMuted: "{r:0.42,g:0.42,b:0.42}", // #6B6B6B
37
+ bgCard: "{r:0.949,g:0.949,b:0.949}", // #F2F2F2
38
+ bgPage: "{r:1,g:1,b:1}", // #FFFFFF
39
+ border: "{r:0.898,g:0.906,b:0.922}", // #E5E7EB
40
+ success: "{r:0.086,g:0.639,b:0.290}", // #16A34A
41
+ error: "{r:0.863,g:0.149,b:0.149}", // #DC2626
42
+ white: "{r:1,g:1,b:1}",
43
+ // Spec annotation colors
44
+ markerOrange: "{r:0.773,g:0.275,b:0}", // #C54600
45
+ padGreen: "{r:0,g:0.490,b:0}", // #007D00
46
+ gapOrange: "{r:0.773,g:0.275,b:0}", // #C54600
47
+ sizeBlue: "{r:0.051,g:0.412,b:0.831}", // #0D69D4
48
+ };
49
+
50
+ type Bridge = {
51
+ execute(code: string): Promise<{ success: boolean; result?: unknown; error?: string }>;
52
+ };
53
+
54
+ function esc(s: string): string { return JSON.stringify(s); }
55
+
56
+ // ── Font loader (Inter with fallbacks) ────────────────────────────────────
57
+
58
+ const FL = `
59
+ var F={};
60
+ async function lf(k,a){for(var i=0;i<a.length;i++){try{await figma.loadFontAsync(a[i]);F[k]=a[i];return}catch(e){}}F[k]={family:"Arial",style:"Regular"};try{await figma.loadFontAsync(F[k])}catch(e){}}
61
+ await lf("b",[{family:"Inter",style:"Bold"},{family:"Roboto",style:"Bold"}]);
62
+ await lf("sb",[{family:"Inter",style:"SemiBold"},{family:"Inter",style:"Medium"},{family:"Roboto",style:"Medium"}]);
63
+ await lf("r",[{family:"Inter",style:"Regular"},{family:"Roboto",style:"Regular"}]);
64
+ await lf("mono",[{family:"SF Mono",style:"Regular"},{family:"Roboto Mono",style:"Regular"},{family:"Courier New",style:"Regular"}]);
65
+ `;
66
+
67
+ // ── Helpers ───────────────────────────────────────────────────────────────
68
+
69
+ function spacerJS(h: number, parent: string = "m"): string {
70
+ return `
71
+ (function(){
72
+ var sp=figma.createFrame();
73
+ sp.resize(4,${h});sp.fills=[];
74
+ ${parent}.appendChild(sp);
75
+ sp.layoutSizingHorizontal="FILL";
76
+ })();`;
77
+ }
78
+
79
+ // Helper: create a text node, append to parent, then set FILL sizing
80
+ function textFillJS(
81
+ varName: string,
82
+ parent: string,
83
+ fontKey: string,
84
+ fontSize: number,
85
+ lineH: number,
86
+ charsExpr: string,
87
+ colorExpr: string,
88
+ extra: string = "",
89
+ ): string {
90
+ return `
91
+ var ${varName}=figma.createText();
92
+ ${varName}.fontName=F.${fontKey};
93
+ ${varName}.fontSize=${fontSize};
94
+ ${varName}.lineHeight={unit:"PIXELS",value:${lineH}};
95
+ ${varName}.characters=${charsExpr};
96
+ ${varName}.fills=[{type:"SOLID",color:${colorExpr}}];
97
+ ${extra}
98
+ ${varName}.textAutoResize="HEIGHT";
99
+ ${parent}.appendChild(${varName});
100
+ ${varName}.layoutSizingHorizontal="FILL";`;
101
+ }
102
+
103
+ // ── Deep style extractor (injected into Figma script for variants) ────────
104
+
105
+ const EXTRACT_STYLE_FN = `
106
+ function extractStyle(node){
107
+ var info={fills:[],texts:[],padding:null,radius:null,w:Math.round(node.width),h:Math.round(node.height)};
108
+ if(Array.isArray(node.fills)){
109
+ for(var fi=0;fi<node.fills.length;fi++){
110
+ var p=node.fills[fi];
111
+ if(p.type==="SOLID"&&p.visible!==false&&p.color){
112
+ var r=Math.round(p.color.r*255),g=Math.round(p.color.g*255),b=Math.round(p.color.b*255);
113
+ info.fills.push("#"+[r,g,b].map(function(v){return v.toString(16).padStart(2,"0")}).join(""));
114
+ }
115
+ }
116
+ }
117
+ if(typeof node.cornerRadius==="number"&&node.cornerRadius>0)info.radius=node.cornerRadius;
118
+ if("paddingTop" in node){
119
+ var pt=node.paddingTop||0,pr=node.paddingRight||0,pb=node.paddingBottom||0,pl=node.paddingLeft||0;
120
+ if(pt+pr+pb+pl>0)info.padding=pt+", "+pr+", "+pb+", "+pl;
121
+ }
122
+ var q=("children" in node)?node.children.slice(0,12):[];
123
+ var depth=0;
124
+ while(q.length>0&&depth<3){
125
+ var next=[];depth++;
126
+ for(var qi=0;qi<q.length;qi++){
127
+ var c=q[qi];
128
+ if(c.type==="TEXT"){
129
+ var fn=c.fontName;
130
+ var family=(fn&&fn!==figma.mixed)?fn.family:"Mixed";
131
+ var style=(fn&&fn!==figma.mixed)?fn.style:"";
132
+ var fs=(typeof c.fontSize==="number")?c.fontSize:0;
133
+ info.texts.push(family+" "+style+" "+fs);
134
+ }
135
+ if(Array.isArray(c.fills)){
136
+ for(var cfi=0;cfi<c.fills.length;cfi++){
137
+ var cp=c.fills[cfi];
138
+ if(cp.type==="SOLID"&&cp.visible!==false&&cp.color){
139
+ var cr=Math.round(cp.color.r*255),cg=Math.round(cp.color.g*255),cb=Math.round(cp.color.b*255);
140
+ var hex="#"+[cr,cg,cb].map(function(v){return v.toString(16).padStart(2,"0")}).join("");
141
+ if(info.fills.indexOf(hex)===-1)info.fills.push(hex);
142
+ }
143
+ }
144
+ }
145
+ if("children" in c&&c.children)for(var sci=0;sci<Math.min(c.children.length,8);sci++)next.push(c.children[sci]);
146
+ }
147
+ q=next;
148
+ }
149
+ var seen={};info.texts=info.texts.filter(function(t){if(seen[t])return false;seen[t]=true;return true;});
150
+ return info;
151
+ }
152
+ `;
153
+
154
+ // ══════════════════════════════════════════════════════════════════════════
155
+ // PHASE 1: LAYOUT FOUNDATION — Card-based sections
156
+ // ══════════════════════════════════════════════════════════════════════════
157
+
158
+ async function initPage(bridge: Bridge, pageName: string): Promise<{ pageId: string; mId: string }> {
159
+ const r = await bridge.execute(`
160
+ (async () => {
161
+ await figma.loadAllPagesAsync();
162
+ var nm=${esc(pageName)};
163
+ var mm=figma.root.children.filter(function(p){return p.name===nm;});
164
+ var pg;
165
+ if(mm.length>0){pg=mm[0];for(var d=1;d<mm.length;d++)mm[d].remove();}
166
+ else{pg=figma.createPage();}
167
+ pg.name=nm;
168
+ await figma.setCurrentPageAsync(pg);
169
+ var ch=pg.children.slice();
170
+ for(var i=0;i<ch.length;i++)ch[i].remove();
171
+
172
+ var m=figma.createFrame();
173
+ m.name="Component Spec";
174
+ m.layoutMode="VERTICAL";
175
+ m.primaryAxisSizingMode="AUTO";
176
+ m.counterAxisSizingMode="FIXED";
177
+ m.resize(${PAGE_W},100);
178
+ m.paddingTop=${PAD};
179
+ m.paddingBottom=${PAD};
180
+ m.paddingLeft=${PAD};
181
+ m.paddingRight=${PAD};
182
+ m.itemSpacing=${SECTION_GAP};
183
+ m.fills=[{type:"SOLID",color:${C.bgPage}}];
184
+ m.clipsContent=false;
185
+ pg.appendChild(m);
186
+ return {pageId:pg.id,mId:m.id};
187
+ })();
188
+ `);
189
+ if (!r.success) throw new Error(`Init: ${r.error}`);
190
+ return r.result as { pageId: string; mId: string };
191
+ }
192
+
193
+ // ── renderDocHeader — Large title + minimal description (no pill badges) ──
194
+
195
+ async function renderDocHeaderAndHero(
196
+ bridge: Bridge, mId: string, name: string, description: string, spec: ComponentSpec,
197
+ sectionLabels?: string[],
198
+ ): Promise<void> {
199
+ // Build subtitle from available sections
200
+ const subtitle = sectionLabels && sectionLabels.length > 0
201
+ ? "Interactive " + sectionLabels.join(", ") + " \u00B7 Component Specification"
202
+ : "Component Specification";
203
+ const compW = spec.extraction.snapshot.width || 200;
204
+ const compH = spec.extraction.snapshot.height || 60;
205
+ const heroScale = Math.min(3, Math.max(1.5, 400 / Math.max(compW, compH)));
206
+ const canvasW = Math.max(600, Math.round(compW * heroScale) + 120);
207
+ const canvasH = Math.round(compH * heroScale) + 80;
208
+
209
+ const r = await bridge.execute(`
210
+ (async () => {
211
+ ${FL}
212
+ var m=await figma.getNodeByIdAsync(${esc(mId)});
213
+ if(!m)return{error:"no master"};
214
+
215
+ // Header card
216
+ var hCard=figma.createFrame();
217
+ hCard.name="Header";
218
+ hCard.layoutMode="VERTICAL";
219
+ hCard.primaryAxisSizingMode="AUTO";
220
+ hCard.counterAxisSizingMode="AUTO";
221
+ hCard.paddingTop=${CARD_PAD};hCard.paddingBottom=${CARD_PAD};
222
+ hCard.paddingLeft=${CARD_PAD};hCard.paddingRight=${CARD_PAD};
223
+ hCard.itemSpacing=16;
224
+ hCard.fills=[{type:"SOLID",color:${C.white}}];
225
+ hCard.cornerRadius=12;
226
+ m.appendChild(hCard);
227
+ hCard.layoutSizingHorizontal="FILL";
228
+
229
+ // 64px title
230
+ var t=figma.createText();
231
+ t.fontName=F.b;t.fontSize=64;
232
+ t.lineHeight={unit:"PIXELS",value:72};
233
+ t.characters=${esc(name)};
234
+ t.fills=[{type:"SOLID",color:${C.textMain}}];
235
+ t.letterSpacing={unit:"PIXELS",value:-2};
236
+ t.textAutoResize="HEIGHT";
237
+ hCard.appendChild(t);
238
+ t.layoutSizingHorizontal="FILL";
239
+
240
+ // Subtitle (section summary)
241
+ var sub=figma.createText();
242
+ sub.fontName=F.r;sub.fontSize=16;
243
+ sub.lineHeight={unit:"PIXELS",value:24};
244
+ sub.characters=${esc(subtitle)};
245
+ sub.fills=[{type:"SOLID",color:${C.textMuted}}];
246
+ sub.letterSpacing={unit:"PIXELS",value:0.3};
247
+ sub.textAutoResize="HEIGHT";
248
+ hCard.appendChild(sub);
249
+ sub.layoutSizingHorizontal="FILL";
250
+
251
+ // Description
252
+ var desc=figma.createText();
253
+ desc.fontName=F.r;desc.fontSize=18;
254
+ desc.lineHeight={unit:"PIXELS",value:28};
255
+ desc.characters=${esc(description)};
256
+ desc.fills=[{type:"SOLID",color:${C.textMuted}}];
257
+ desc.textAutoResize="HEIGHT";
258
+ hCard.appendChild(desc);
259
+ desc.layoutSizingHorizontal="FILL";
260
+
261
+ // Hero section (merged into same call)
262
+ var sourceNode=await figma.getNodeByIdAsync(${esc(spec.nodeId)});
263
+ if(sourceNode){
264
+ var heroCard=figma.createFrame();heroCard.name="Hero";
265
+ heroCard.layoutMode="VERTICAL";
266
+ heroCard.primaryAxisSizingMode="AUTO";heroCard.counterAxisSizingMode="AUTO";
267
+ heroCard.itemSpacing=0;heroCard.fills=[];
268
+ m.appendChild(heroCard);heroCard.layoutSizingHorizontal="FILL";
269
+
270
+ var canvas=figma.createFrame();canvas.name="Hero Canvas";
271
+ canvas.layoutMode="HORIZONTAL";
272
+ canvas.primaryAxisSizingMode="FIXED";canvas.counterAxisSizingMode="FIXED";
273
+ canvas.resize(${canvasW},${canvasH});
274
+ canvas.fills=[{type:"SOLID",color:${C.bgCard}}];canvas.cornerRadius=12;
275
+ canvas.primaryAxisAlignItems="CENTER";canvas.counterAxisAlignItems="CENTER";
276
+ canvas.clipsContent=true;
277
+ heroCard.appendChild(canvas);canvas.layoutSizingHorizontal="FILL";
278
+
279
+ var src=sourceNode;
280
+ if(src.type==="COMPONENT_SET"&&src.children.length>0)src=src.children[0];
281
+ var clone=src.clone();
282
+ ${heroScale > 1 ? `clone.rescale(${heroScale});` : ""}
283
+ canvas.appendChild(clone);
284
+ }
285
+
286
+ return{ok:true};
287
+ })();
288
+ `);
289
+ if (!r.success) console.error("Header+Hero failed:", r.error);
290
+ }
291
+
292
+ // ── Section card wrapper — white card with 64px padding ──────────────────
293
+
294
+ function sectionCardOpenJS(varName: string, parent: string, sectionName: string): string {
295
+ return `
296
+ var ${varName}=figma.createFrame();
297
+ ${varName}.name=${esc(sectionName)};
298
+ ${varName}.layoutMode="VERTICAL";
299
+ ${varName}.primaryAxisSizingMode="AUTO";
300
+ ${varName}.counterAxisSizingMode="AUTO";
301
+ ${varName}.paddingTop=0;${varName}.paddingBottom=${CARD_PAD};
302
+ ${varName}.paddingLeft=${CARD_PAD};${varName}.paddingRight=${CARD_PAD};
303
+ ${varName}.itemSpacing=${CARD_ITEM_GAP};
304
+ ${varName}.fills=[{type:"SOLID",color:${C.white}}];
305
+ ${varName}.cornerRadius=12;
306
+ ${varName}.clipsContent=true;
307
+ ${parent}.appendChild(${varName});
308
+ ${varName}.layoutSizingHorizontal="FILL";
309
+
310
+ // Accent bar at top of card
311
+ var ${varName}Accent=figma.createFrame();
312
+ ${varName}Accent.name="Accent";
313
+ ${varName}Accent.resize(4,3);
314
+ ${varName}Accent.fills=[{type:"SOLID",color:${C.markerOrange}}];
315
+ ${varName}.appendChild(${varName}Accent);
316
+ ${varName}Accent.layoutSizingHorizontal="FILL";
317
+
318
+ // Top padding spacer (replaces paddingTop since accent bar is first child)
319
+ var ${varName}TopPad=figma.createFrame();
320
+ ${varName}TopPad.name="TopPad";
321
+ ${varName}TopPad.resize(4,${CARD_PAD - 3});
322
+ ${varName}TopPad.fills=[];
323
+ ${varName}.appendChild(${varName}TopPad);
324
+ ${varName}TopPad.layoutSizingHorizontal="FILL";`;
325
+ }
326
+
327
+ // ── Section title — 48px Inter Bold ──────────────────────────────────────
328
+
329
+ function sectionTitleJS(varName: string, parent: string, title: string, sectionNum?: number): string {
330
+ const displayTitle = sectionNum != null
331
+ ? String(sectionNum).padStart(2, "0") + " " + title
332
+ : title;
333
+ return `
334
+ var ${varName}=figma.createText();
335
+ ${varName}.fontName=F.b;${varName}.fontSize=28;
336
+ ${varName}.lineHeight={unit:"PIXELS",value:36};
337
+ ${varName}.characters=${esc(displayTitle)};
338
+ ${varName}.fills=[{type:"SOLID",color:${C.textMain}}];
339
+ ${varName}.letterSpacing={unit:"PIXELS",value:-0.5};
340
+ ${varName}.textAutoResize="HEIGHT";
341
+ ${parent}.appendChild(${varName});
342
+ ${varName}.layoutSizingHorizontal="FILL";`;
343
+ }
344
+
345
+ // ── Subsection title — 36px Inter Bold ───────────────────────────────────
346
+
347
+ function subsectionTitleJS(varName: string, parent: string, title: string): string {
348
+ return `
349
+ var ${varName}=figma.createText();
350
+ ${varName}.fontName=F.b;${varName}.fontSize=20;
351
+ ${varName}.lineHeight={unit:"PIXELS",value:28};
352
+ ${varName}.characters=${esc(title)};
353
+ ${varName}.fills=[{type:"SOLID",color:${C.textMain}}];
354
+ ${varName}.letterSpacing={unit:"PIXELS",value:0};
355
+ ${varName}.textAutoResize="HEIGHT";
356
+ ${parent}.appendChild(${varName});
357
+ ${varName}.layoutSizingHorizontal="FILL";`;
358
+ }
359
+
360
+ // ══════════════════════════════════════════════════════════════════════════
361
+ // TEXT-BASED SUB-RENDERERS (for knowledge sections)
362
+ // ══════════════════════════════════════════════════════════════════════════
363
+
364
+ async function renderTextSection(
365
+ bridge: Bridge, mId: string, title: string, content: SpecSectionContent, sectionNum?: number,
366
+ ): Promise<void> {
367
+ // Create a section card, add title, then render content inside it
368
+ const contentJS = buildContentJS("sc", content);
369
+
370
+ const r = await bridge.execute(`
371
+ (async () => {
372
+ ${FL}
373
+ var m=await figma.getNodeByIdAsync(${esc(mId)});
374
+ if(!m)return{error:"no master"};
375
+
376
+ ${sectionCardOpenJS("sc", "m", title)}
377
+ ${sectionTitleJS("stitle", "sc", title, sectionNum)}
378
+ ${contentJS}
379
+
380
+ return{ok:true};
381
+ })();
382
+ `);
383
+ if (!r.success) console.error(`Text section "${title}" failed:`, r.error);
384
+ }
385
+
386
+ function buildContentJS(parent: string, content: SpecSectionContent): string {
387
+ switch (content.kind) {
388
+ case "paragraph":
389
+ return textFillJS("_p", parent, "r", 16, 26, esc(content.text), C.textMuted);
390
+
391
+ case "key-value": {
392
+ const capped = content.entries.slice(0, 40);
393
+ return `
394
+ var _kvOuter=figma.createFrame();_kvOuter.name="Key-Value";
395
+ _kvOuter.layoutMode="VERTICAL";
396
+ _kvOuter.primaryAxisSizingMode="AUTO";_kvOuter.counterAxisSizingMode="AUTO";
397
+ _kvOuter.itemSpacing=0;_kvOuter.fills=[];
398
+ ${parent}.appendChild(_kvOuter);_kvOuter.layoutSizingHorizontal="FILL";
399
+ var _kvEntries=${JSON.stringify(capped)};
400
+ for(var _kvi=0;_kvi<_kvEntries.length;_kvi++){
401
+ var _kvRow=figma.createFrame();_kvRow.name=_kvEntries[_kvi].label;
402
+ _kvRow.layoutMode="HORIZONTAL";
403
+ _kvRow.primaryAxisSizingMode="AUTO";_kvRow.counterAxisSizingMode="AUTO";
404
+ _kvRow.itemSpacing=16;_kvRow.paddingTop=10;_kvRow.paddingBottom=10;
405
+ _kvRow.fills=[];_kvRow.counterAxisAlignItems="MIN";
406
+ _kvRow.strokes=[{type:"SOLID",color:${C.border}}];
407
+ _kvRow.strokeWeight=1;_kvRow.strokeTopWeight=0;_kvRow.strokeRightWeight=0;
408
+ _kvRow.strokeBottomWeight=1;_kvRow.strokeLeftWeight=0;_kvRow.strokeAlign="INSIDE";
409
+ _kvOuter.appendChild(_kvRow);_kvRow.layoutSizingHorizontal="FILL";
410
+ var _kvLbl=figma.createText();_kvLbl.fontName=F.sb;_kvLbl.fontSize=14;
411
+ _kvLbl.lineHeight={unit:"PIXELS",value:20};
412
+ _kvLbl.characters=_kvEntries[_kvi].label;
413
+ _kvLbl.fills=[{type:"SOLID",color:${C.textMuted}}];
414
+ _kvLbl.textAutoResize="HEIGHT";_kvLbl.resize(200,20);
415
+ _kvRow.appendChild(_kvLbl);_kvLbl.layoutSizingHorizontal="FIXED";
416
+ var _kvVal=figma.createText();_kvVal.fontName=F.r;_kvVal.fontSize=16;
417
+ _kvVal.lineHeight={unit:"PIXELS",value:24};
418
+ _kvVal.characters=_kvEntries[_kvi].value||"\\u2014";
419
+ _kvVal.fills=[{type:"SOLID",color:${C.textMain}}];
420
+ _kvVal.textAutoResize="HEIGHT";
421
+ _kvRow.appendChild(_kvVal);_kvVal.layoutSizingHorizontal="FILL";
422
+ }`;
423
+ }
424
+
425
+ case "table":
426
+ case "structured-data": {
427
+ const headers = content.kind === "table" ? content.headers : content.columns;
428
+ const rows = content.kind === "table"
429
+ ? content.rows.slice(0, 50)
430
+ : content.rows.slice(0, 50).map(r => headers.map(c => r[c] ?? "\u2014"));
431
+ const colCount = headers.length;
432
+ const cellW = Math.floor((CONTENT_W - CARD_PAD * 2 - 32) / colCount);
433
+ return `
434
+ var _tHeaders=${JSON.stringify(headers)};
435
+ var _tRows=${JSON.stringify(rows)};
436
+ var _tCellW=${cellW};
437
+ var _tbl=figma.createFrame();_tbl.name="Table";
438
+ _tbl.layoutMode="VERTICAL";
439
+ _tbl.primaryAxisSizingMode="AUTO";_tbl.counterAxisSizingMode="AUTO";
440
+ _tbl.itemSpacing=0;_tbl.fills=[{type:"SOLID",color:${C.white}}];
441
+ _tbl.cornerRadius=8;
442
+ _tbl.strokes=[{type:"SOLID",color:${C.border}}];_tbl.strokeWeight=1;_tbl.strokeAlign="INSIDE";
443
+ _tbl.clipsContent=true;
444
+ ${parent}.appendChild(_tbl);_tbl.layoutSizingHorizontal="FILL";
445
+ var _tHdr=figma.createFrame();_tHdr.name="Header";
446
+ _tHdr.layoutMode="HORIZONTAL";
447
+ _tHdr.primaryAxisSizingMode="AUTO";_tHdr.counterAxisSizingMode="AUTO";
448
+ _tHdr.itemSpacing=0;_tHdr.paddingTop=12;_tHdr.paddingBottom=12;
449
+ _tHdr.paddingLeft=16;_tHdr.paddingRight=16;
450
+ _tHdr.fills=[{type:"SOLID",color:${C.bgCard}}];_tHdr.counterAxisAlignItems="MIN";
451
+ _tHdr.strokes=[{type:"SOLID",color:${C.border}}];_tHdr.strokeWeight=1;
452
+ _tHdr.strokeTopWeight=0;_tHdr.strokeRightWeight=0;_tHdr.strokeBottomWeight=1;_tHdr.strokeLeftWeight=0;
453
+ _tHdr.strokeAlign="INSIDE";
454
+ _tbl.appendChild(_tHdr);_tHdr.layoutSizingHorizontal="FILL";
455
+ for(var _hc=0;_hc<_tHeaders.length;_hc++){
456
+ var _ht=figma.createText();_ht.fontName=F.sb;_ht.fontSize=14;
457
+ _ht.lineHeight={unit:"PIXELS",value:20};_ht.characters=_tHeaders[_hc]||"";
458
+ _ht.fills=[{type:"SOLID",color:${C.textMain}}];_ht.textAutoResize="HEIGHT";
459
+ _ht.resize(_tCellW,20);_tHdr.appendChild(_ht);_ht.layoutSizingHorizontal="FIXED";
460
+ }
461
+ for(var _ri=0;_ri<_tRows.length;_ri++){
462
+ var _tRow=figma.createFrame();_tRow.name="Row "+(_ri+1);
463
+ _tRow.layoutMode="HORIZONTAL";
464
+ _tRow.primaryAxisSizingMode="AUTO";_tRow.counterAxisSizingMode="AUTO";
465
+ _tRow.itemSpacing=0;_tRow.paddingTop=10;_tRow.paddingBottom=10;
466
+ _tRow.paddingLeft=16;_tRow.paddingRight=16;_tRow.fills=[];_tRow.counterAxisAlignItems="MIN";
467
+ if(_ri<_tRows.length-1){
468
+ _tRow.strokes=[{type:"SOLID",color:${C.border}}];_tRow.strokeWeight=1;
469
+ _tRow.strokeTopWeight=0;_tRow.strokeRightWeight=0;_tRow.strokeBottomWeight=1;_tRow.strokeLeftWeight=0;
470
+ _tRow.strokeAlign="INSIDE";
471
+ }
472
+ _tbl.appendChild(_tRow);_tRow.layoutSizingHorizontal="FILL";
473
+ for(var _rc=0;_rc<_tHeaders.length;_rc++){
474
+ var _ct=figma.createText();_ct.fontName=F.r;_ct.fontSize=14;
475
+ _ct.lineHeight={unit:"PIXELS",value:20};
476
+ _ct.characters=(_tRows[_ri][_rc]!=null?_tRows[_ri][_rc]:"")||"\\u2014";
477
+ _ct.fills=[{type:"SOLID",color:${C.textMuted}}];_ct.textAutoResize="HEIGHT";
478
+ _ct.resize(_tCellW,20);_tRow.appendChild(_ct);_ct.layoutSizingHorizontal="FIXED";
479
+ }
480
+ }`;
481
+ }
482
+
483
+ case "list":
484
+ case "rules": {
485
+ const items = content.items.slice(0, 60);
486
+ return `
487
+ var _lItems=${JSON.stringify(items)};
488
+ var _lOuter=figma.createFrame();_lOuter.name="List";
489
+ _lOuter.layoutMode="VERTICAL";
490
+ _lOuter.primaryAxisSizingMode="AUTO";_lOuter.counterAxisSizingMode="AUTO";
491
+ _lOuter.itemSpacing=6;_lOuter.fills=[];_lOuter.paddingLeft=8;
492
+ ${parent}.appendChild(_lOuter);_lOuter.layoutSizingHorizontal="FILL";
493
+ for(var _li=0;_li<_lItems.length;_li++){
494
+ var _lRow=figma.createFrame();_lRow.name="Item "+(_li+1);
495
+ _lRow.layoutMode="HORIZONTAL";
496
+ _lRow.primaryAxisSizingMode="AUTO";_lRow.counterAxisSizingMode="AUTO";
497
+ _lRow.itemSpacing=8;_lRow.fills=[];_lRow.counterAxisAlignItems="MIN";
498
+ _lOuter.appendChild(_lRow);_lRow.layoutSizingHorizontal="FILL";
499
+ var _lBul=figma.createText();_lBul.fontName=F.r;_lBul.fontSize=16;
500
+ _lBul.lineHeight={unit:"PIXELS",value:26};_lBul.characters="\\u2022";
501
+ _lBul.fills=[{type:"SOLID",color:${C.textMuted}}];_lBul.textAutoResize="WIDTH_AND_HEIGHT";
502
+ _lRow.appendChild(_lBul);
503
+ var _lTxt=figma.createText();_lTxt.fontName=F.r;_lTxt.fontSize=16;
504
+ _lTxt.lineHeight={unit:"PIXELS",value:26};_lTxt.characters=_lItems[_li]||"\\u2014";
505
+ _lTxt.fills=[{type:"SOLID",color:${C.textMuted}}];_lTxt.textAutoResize="HEIGHT";
506
+ _lRow.appendChild(_lTxt);_lTxt.layoutSizingHorizontal="FILL";
507
+ }`;
508
+ }
509
+
510
+ case "do-dont": {
511
+ const cappedDos = content.dos.slice(0, 30);
512
+ const cappedDonts = content.donts.slice(0, 30);
513
+ return `
514
+ var _ddDos=${JSON.stringify(cappedDos)};
515
+ var _ddDonts=${JSON.stringify(cappedDonts)};
516
+ var _ddOuter=figma.createFrame();_ddOuter.name="Do-Dont";
517
+ _ddOuter.layoutMode="HORIZONTAL";
518
+ _ddOuter.primaryAxisSizingMode="AUTO";_ddOuter.counterAxisSizingMode="AUTO";
519
+ _ddOuter.itemSpacing=24;_ddOuter.fills=[];_ddOuter.counterAxisAlignItems="MIN";
520
+ ${parent}.appendChild(_ddOuter);_ddOuter.layoutSizingHorizontal="FILL";
521
+
522
+ function _ddBuildCol(parent,title,items,iconChar,iconColor,titleColor){
523
+ var col=figma.createFrame();col.name=title;
524
+ col.layoutMode="VERTICAL";
525
+ col.primaryAxisSizingMode="AUTO";col.counterAxisSizingMode="AUTO";
526
+ col.itemSpacing=8;col.paddingTop=16;col.paddingBottom=16;
527
+ col.paddingLeft=16;col.paddingRight=16;
528
+ col.fills=[{type:"SOLID",color:${C.bgCard}}];col.cornerRadius=8;
529
+ parent.appendChild(col);col.layoutSizingHorizontal="FILL";
530
+ var ct=figma.createText();ct.fontName=F.sb;ct.fontSize=16;
531
+ ct.lineHeight={unit:"PIXELS",value:22};ct.characters=iconChar+" "+title;
532
+ ct.fills=[{type:"SOLID",color:titleColor}];ct.textAutoResize="WIDTH_AND_HEIGHT";
533
+ col.appendChild(ct);
534
+ var sp=figma.createFrame();sp.resize(4,4);sp.fills=[];col.appendChild(sp);sp.layoutSizingHorizontal="FILL";
535
+ for(var i=0;i<items.length;i++){
536
+ var row=figma.createFrame();row.name=title+" "+(i+1);
537
+ row.layoutMode="HORIZONTAL";
538
+ row.primaryAxisSizingMode="AUTO";row.counterAxisSizingMode="AUTO";
539
+ row.itemSpacing=8;row.fills=[];row.counterAxisAlignItems="MIN";
540
+ col.appendChild(row);row.layoutSizingHorizontal="FILL";
541
+ var icon=figma.createText();icon.fontName=F.sb;icon.fontSize=14;
542
+ icon.lineHeight={unit:"PIXELS",value:22};icon.characters=iconChar;
543
+ icon.fills=[{type:"SOLID",color:iconColor}];icon.textAutoResize="WIDTH_AND_HEIGHT";
544
+ row.appendChild(icon);
545
+ var txt=figma.createText();txt.fontName=F.r;txt.fontSize=14;
546
+ txt.lineHeight={unit:"PIXELS",value:22};txt.characters=items[i]||"\\u2014";
547
+ txt.fills=[{type:"SOLID",color:${C.textMuted}}];txt.textAutoResize="HEIGHT";
548
+ row.appendChild(txt);txt.layoutSizingHorizontal="FILL";
549
+ }
550
+ }
551
+ if(_ddDos.length>0)_ddBuildCol(_ddOuter,"Do",_ddDos,"\\u2713",${C.success},${C.success});
552
+ if(_ddDonts.length>0)_ddBuildCol(_ddOuter,"Don\\u2019t",_ddDonts,"\\u2717",${C.error},${C.error});`;
553
+ }
554
+
555
+ case "mixed":
556
+ return content.blocks.map(b => buildContentJS(parent, b)).join("\n");
557
+
558
+ default:
559
+ return "";
560
+ }
561
+ }
562
+
563
+ // ══════════════════════════════════════════════════════════════════════════
564
+ // PHASE 2: EXHIBIT RENDERER — Reusable artwork + content pattern
565
+ // ══════════════════════════════════════════════════════════════════════════
566
+
567
+ /**
568
+ * Generate JS for a single exhibit: gray artwork left + content metadata right.
569
+ * The artwork frame is absolute-positioned for component placement;
570
+ * content is an auto-layout column with label, element info, and attributes.
571
+ */
572
+ function exhibitOpenJS(
573
+ varName: string,
574
+ parent: string,
575
+ label: string,
576
+ artworkVar: string,
577
+ contentVar: string,
578
+ ): string {
579
+ return `
580
+ var ${varName}=figma.createFrame();${varName}.name=${esc(label)};
581
+ ${varName}.layoutMode="HORIZONTAL";
582
+ ${varName}.primaryAxisSizingMode="AUTO";${varName}.counterAxisSizingMode="AUTO";
583
+ ${varName}.itemSpacing=${EXHIBIT_GAP};
584
+ ${varName}.fills=[];
585
+ ${varName}.counterAxisAlignItems="MIN";
586
+ ${parent}.appendChild(${varName});${varName}.layoutSizingHorizontal="FILL";
587
+
588
+ var ${artworkVar}=figma.createFrame();${artworkVar}.name="Artwork";
589
+ ${artworkVar}.layoutMode="HORIZONTAL";
590
+ ${artworkVar}.primaryAxisSizingMode="FIXED";${artworkVar}.counterAxisSizingMode="FIXED";
591
+ ${artworkVar}.resize(${ARTWORK_W},${ARTWORK_H});
592
+ ${artworkVar}.fills=[{type:"SOLID",color:${C.bgCard}}];
593
+ ${artworkVar}.cornerRadius=8;
594
+ ${artworkVar}.primaryAxisAlignItems="CENTER";${artworkVar}.counterAxisAlignItems="CENTER";
595
+ ${artworkVar}.clipsContent=true;
596
+ ${varName}.appendChild(${artworkVar});
597
+
598
+ var ${contentVar}=figma.createFrame();${contentVar}.name="Content";
599
+ ${contentVar}.layoutMode="VERTICAL";
600
+ ${contentVar}.primaryAxisSizingMode="AUTO";${contentVar}.counterAxisSizingMode="AUTO";
601
+ ${contentVar}.itemSpacing=16;${contentVar}.fills=[];
602
+ ${varName}.appendChild(${contentVar});${contentVar}.layoutSizingHorizontal="FILL";`;
603
+ }
604
+
605
+ /**
606
+ * Generate JS for a type icon (20x20 frame with node type indicator)
607
+ */
608
+ function typeIconJS(varName: string, parent: string, nodeType: string): string {
609
+ let glyph = "\u25A1"; // default: square for FRAME
610
+ if (nodeType === "INSTANCE") glyph = "\u25C7"; // diamond
611
+ else if (nodeType === "TEXT") glyph = "T";
612
+
613
+ return `
614
+ var ${varName}=figma.createFrame();${varName}.name="TypeIcon";
615
+ ${varName}.layoutMode="HORIZONTAL";
616
+ ${varName}.primaryAxisSizingMode="FIXED";${varName}.counterAxisSizingMode="FIXED";
617
+ ${varName}.resize(20,20);
618
+ ${varName}.fills=[];
619
+ ${varName}.strokes=[{type:"SOLID",color:${C.textMuted}}];
620
+ ${varName}.strokeWeight=1;${varName}.strokeAlign="INSIDE";
621
+ ${varName}.cornerRadius=3;
622
+ ${varName}.primaryAxisAlignItems="CENTER";${varName}.counterAxisAlignItems="CENTER";
623
+ ${parent}.appendChild(${varName});
624
+ var ${varName}Lbl=figma.createText();${varName}Lbl.fontName=F.r;${varName}Lbl.fontSize=10;
625
+ ${varName}Lbl.characters=${esc(glyph)};
626
+ ${varName}Lbl.fills=[{type:"SOLID",color:${C.textMuted}}];
627
+ ${varName}Lbl.textAutoResize="WIDTH_AND_HEIGHT";
628
+ ${varName}.appendChild(${varName}Lbl);`;
629
+ }
630
+
631
+ /**
632
+ * Generate JS for an attribute row (name: value)
633
+ */
634
+ function attrRowJS(varName: string, parent: string, name: string, value: string): string {
635
+ return `
636
+ var ${varName}=figma.createFrame();${varName}.name=${esc(name)};
637
+ ${varName}.layoutMode="HORIZONTAL";
638
+ ${varName}.primaryAxisSizingMode="AUTO";${varName}.counterAxisSizingMode="AUTO";
639
+ ${varName}.itemSpacing=4;${varName}.fills=[];
640
+ ${parent}.appendChild(${varName});${varName}.layoutSizingHorizontal="FILL";
641
+ var ${varName}N=figma.createText();${varName}N.fontName=F.r;${varName}N.fontSize=11;
642
+ ${varName}N.lineHeight={unit:"PIXELS",value:16};
643
+ ${varName}N.characters=${esc(name + ":")};
644
+ ${varName}N.fills=[{type:"SOLID",color:${C.textMuted}}];
645
+ ${varName}N.textAutoResize="WIDTH_AND_HEIGHT";
646
+ ${varName}N.resize(90,16);
647
+ ${varName}.appendChild(${varName}N);
648
+ ${varName}N.layoutSizingHorizontal="FIXED";
649
+ var ${varName}V=figma.createText();${varName}V.fontName=F.sb;${varName}V.fontSize=11;
650
+ ${varName}V.lineHeight={unit:"PIXELS",value:16};
651
+ ${varName}V.characters=${esc(value)};
652
+ ${varName}V.fills=[{type:"SOLID",color:${C.textMain}}];
653
+ ${varName}V.textAutoResize="WIDTH_AND_HEIGHT";
654
+ ${varName}.appendChild(${varName}V);`;
655
+ }
656
+
657
+ // ══════════════════════════════════════════════════════════════════════════
658
+ // PHASE 4: ANATOMY EXHIBIT — Orange markers, type icons, attributes
659
+ // ══════════════════════════════════════════════════════════════════════════
660
+
661
+ async function renderAnatomyExhibit(
662
+ bridge: Bridge, mId: string, spec: ComponentSpec, sectionNum?: number,
663
+ ): Promise<void> {
664
+ const anatomy = spec.extraction.anatomy;
665
+ const elements = anatomy.elements.filter(
666
+ el => el.visible && el.position.w > 0 && el.position.h > 0,
667
+ );
668
+ if (elements.length === 0) return;
669
+
670
+ const compW = anatomy.componentBounds?.w || spec.extraction.snapshot.width || 200;
671
+ const compH = anatomy.componentBounds?.h || spec.extraction.snapshot.height || 60;
672
+
673
+ // Scale up small components
674
+ const scale = Math.min(3, Math.max(1, 140 / Math.min(compW, compH)));
675
+ const displayW = Math.round(compW * scale);
676
+ const displayH = Math.round(compH * scale);
677
+
678
+ const scaledElements = elements.map(el => ({
679
+ ...el,
680
+ position: {
681
+ x: Math.round(el.position.x * scale),
682
+ y: Math.round(el.position.y * scale),
683
+ w: Math.round(el.position.w * scale),
684
+ h: Math.round(el.position.h * scale),
685
+ },
686
+ }));
687
+
688
+ const MARGIN = 60;
689
+ const markers = computeMarkerPositions(displayW, displayH, MARGIN, MARGIN, scaledElements);
690
+ if (markers.length === 0) return;
691
+
692
+ // Build anatomy content items JS
693
+ const anatomyItemsJS = elements.slice(0, markers.length).map((el, idx) => {
694
+ const num = idx + 1;
695
+ const vPrefix = `_ai${num}`;
696
+ let attributeLines = "";
697
+
698
+ if (el.nodeType === "TEXT") {
699
+ if (el.fontFamily) attributeLines += attrRowJS(`${vPrefix}a1`, `${vPrefix}col`, "Font family", el.fontFamily);
700
+ if (el.fontStyle) attributeLines += attrRowJS(`${vPrefix}a2`, `${vPrefix}col`, "Font weight", el.fontStyle);
701
+ if (el.fontSize) attributeLines += attrRowJS(`${vPrefix}a3`, `${vPrefix}col`, "Font size", String(el.fontSize) + "px");
702
+ if (el.lineHeightPx) attributeLines += attrRowJS(`${vPrefix}a4`, `${vPrefix}col`, "Line height", String(el.lineHeightPx) + "px");
703
+ if (el.tokenName) attributeLines += attrRowJS(`${vPrefix}a5`, `${vPrefix}col`, "Text style", el.tokenName);
704
+ if (el.fills?.[0]) attributeLines += attrRowJS(`${vPrefix}a6`, `${vPrefix}col`, "Fill", el.fills[0]);
705
+ } else if (el.nodeType === "INSTANCE") {
706
+ if (el.componentName) attributeLines += attrRowJS(`${vPrefix}a1`, `${vPrefix}col`, "Depends on", el.componentName);
707
+ if (el.instanceOf && el.instanceOf !== el.componentName) attributeLines += attrRowJS(`${vPrefix}a2`, `${vPrefix}col`, "Variant", el.instanceOf);
708
+ if (el.position?.w) attributeLines += attrRowJS(`${vPrefix}a3`, `${vPrefix}col`, "Size", `${el.position.w}\u00D7${el.position.h}`);
709
+ if (el.variantProperties) {
710
+ const vpEntries = Object.entries(el.variantProperties).slice(0, 3);
711
+ vpEntries.forEach(([k, v], i) => {
712
+ attributeLines += attrRowJS(`${vPrefix}vp${i}`, `${vPrefix}col`, k, v);
713
+ });
714
+ }
715
+ if (el.fills?.[0]) attributeLines += attrRowJS(`${vPrefix}a4`, `${vPrefix}col`, "Fill", el.fills[0]);
716
+ } else if (el.nodeType === "FRAME" || el.nodeType === "COMPONENT") {
717
+ if (el.layoutMode && el.layoutMode !== "NONE") attributeLines += attrRowJS(`${vPrefix}a1`, `${vPrefix}col`, "Direction", el.layoutMode === "HORIZONTAL" ? "Horizontal" : "Vertical");
718
+ if (el.itemSpacing != null && el.itemSpacing > 0) attributeLines += attrRowJS(`${vPrefix}a2`, `${vPrefix}col`, "Item spacing", String(el.itemSpacing) + "px");
719
+ if (el.cornerRadius != null && el.cornerRadius > 0) attributeLines += attrRowJS(`${vPrefix}a3`, `${vPrefix}col`, "Radius", String(el.cornerRadius) + "px");
720
+ if (el.fills?.[0]) attributeLines += attrRowJS(`${vPrefix}a4`, `${vPrefix}col`, "Fill", el.fills[0]);
721
+ if (el.strokes?.[0]) attributeLines += attrRowJS(`${vPrefix}a5`, `${vPrefix}col`, "Stroke", el.strokes[0]);
722
+ } else {
723
+ if (el.fills?.[0]) attributeLines += attrRowJS(`${vPrefix}a1`, `${vPrefix}col`, "Fill", el.fills[0]);
724
+ if (el.strokes?.[0]) attributeLines += attrRowJS(`${vPrefix}a2`, `${vPrefix}col`, "Stroke", el.strokes[0]);
725
+ if (el.cornerRadius != null && el.cornerRadius > 0) attributeLines += attrRowJS(`${vPrefix}a3`, `${vPrefix}col`, "Radius", String(el.cornerRadius) + "px");
726
+ }
727
+ // Size for all non-instance elements
728
+ if (el.nodeType !== "INSTANCE" && el.position?.w > 0) {
729
+ attributeLines += attrRowJS(`${vPrefix}sz`, `${vPrefix}col`, "Size", `${el.position.w}\u00D7${el.position.h}`);
730
+ }
731
+
732
+ return `
733
+ // Anatomy item ${num}
734
+ var ${vPrefix}=figma.createFrame();${vPrefix}.name="Item ${num}";
735
+ ${vPrefix}.layoutMode="HORIZONTAL";
736
+ ${vPrefix}.primaryAxisSizingMode="AUTO";${vPrefix}.counterAxisSizingMode="AUTO";
737
+ ${vPrefix}.itemSpacing=8;${vPrefix}.fills=[];${vPrefix}.counterAxisAlignItems="MIN";
738
+ ${vPrefix}.paddingLeft=28;
739
+ anatContent.appendChild(${vPrefix});${vPrefix}.layoutSizingHorizontal="FILL";
740
+
741
+ // Orange numbered dot (overlapping left edge)
742
+ var ${vPrefix}dot=figma.createEllipse();${vPrefix}dot.resize(20,20);
743
+ ${vPrefix}dot.fills=[{type:"SOLID",color:${C.markerOrange}}];
744
+ ${vPrefix}dot.name="Dot ${num}";
745
+
746
+ var ${vPrefix}dotWrap=figma.createFrame();${vPrefix}dotWrap.name="DotWrap";
747
+ ${vPrefix}dotWrap.layoutMode="NONE";${vPrefix}dotWrap.resize(20,20);${vPrefix}dotWrap.fills=[];
748
+ ${vPrefix}dotWrap.appendChild(${vPrefix}dot);${vPrefix}dot.x=0;${vPrefix}dot.y=0;
749
+ var ${vPrefix}dotNum=figma.createText();${vPrefix}dotNum.fontName=F.b;${vPrefix}dotNum.fontSize=12;
750
+ ${vPrefix}dotNum.characters="${num}";
751
+ ${vPrefix}dotNum.fills=[{type:"SOLID",color:{r:1,g:1,b:1}}];
752
+ ${vPrefix}dotNum.textAutoResize="WIDTH_AND_HEIGHT";
753
+ ${vPrefix}dotWrap.appendChild(${vPrefix}dotNum);
754
+ ${vPrefix}dotNum.x=10-${vPrefix}dotNum.width/2;
755
+ ${vPrefix}dotNum.y=10-${vPrefix}dotNum.height/2;
756
+ ${vPrefix}.appendChild(${vPrefix}dotWrap);
757
+
758
+ // Element info column
759
+ var ${vPrefix}col=figma.createFrame();${vPrefix}col.name="Info";
760
+ ${vPrefix}col.layoutMode="VERTICAL";
761
+ ${vPrefix}col.primaryAxisSizingMode="AUTO";${vPrefix}col.counterAxisSizingMode="AUTO";
762
+ ${vPrefix}col.itemSpacing=4;${vPrefix}col.fills=[];
763
+ ${vPrefix}.appendChild(${vPrefix}col);${vPrefix}col.layoutSizingHorizontal="FILL";
764
+
765
+ // Element row: type icon + name
766
+ var ${vPrefix}elRow=figma.createFrame();${vPrefix}elRow.name="ElRow";
767
+ ${vPrefix}elRow.layoutMode="HORIZONTAL";
768
+ ${vPrefix}elRow.primaryAxisSizingMode="AUTO";${vPrefix}elRow.counterAxisSizingMode="AUTO";
769
+ ${vPrefix}elRow.itemSpacing=6;${vPrefix}elRow.fills=[];${vPrefix}elRow.counterAxisAlignItems="CENTER";
770
+ ${vPrefix}col.appendChild(${vPrefix}elRow);${vPrefix}elRow.layoutSizingHorizontal="FILL";
771
+
772
+ ${typeIconJS(`${vPrefix}icon`, `${vPrefix}elRow`, el.nodeType)}
773
+
774
+ var ${vPrefix}name=figma.createText();${vPrefix}name.fontName=F.b;${vPrefix}name.fontSize=16;
775
+ ${vPrefix}name.lineHeight={unit:"PIXELS",value:22};
776
+ ${vPrefix}name.characters=${esc(el.name)};
777
+ ${vPrefix}name.fills=[{type:"SOLID",color:${C.textMain}}];
778
+ ${vPrefix}name.textAutoResize="WIDTH_AND_HEIGHT";
779
+ ${vPrefix}elRow.appendChild(${vPrefix}name);
780
+
781
+ ${attributeLines}
782
+ `;
783
+ }).join("\n");
784
+
785
+ const markersJSON = JSON.stringify(markers);
786
+ const MD = 24; // marker diameter
787
+
788
+ const r = await bridge.execute(`
789
+ (async () => {
790
+ ${FL}
791
+ var m=await figma.getNodeByIdAsync(${esc(mId)});
792
+ if(!m)return{error:"no master"};
793
+ var sourceNode=await figma.getNodeByIdAsync(${esc(spec.nodeId)});
794
+ if(!sourceNode)return{error:"no source"};
795
+
796
+ // Section card
797
+ ${sectionCardOpenJS("anatCard", "m", "Anatomy")}
798
+ ${sectionTitleJS("anatTitle", "anatCard", "Anatomy", sectionNum)}
799
+
800
+ // Exhibit: artwork left | content right
801
+ var anatExhibit=figma.createFrame();anatExhibit.name="Anatomy Exhibit";
802
+ anatExhibit.layoutMode="HORIZONTAL";
803
+ anatExhibit.primaryAxisSizingMode="AUTO";anatExhibit.counterAxisSizingMode="AUTO";
804
+ anatExhibit.itemSpacing=${EXHIBIT_GAP};anatExhibit.fills=[];
805
+ anatExhibit.counterAxisAlignItems="MIN";
806
+ anatCard.appendChild(anatExhibit);anatExhibit.layoutSizingHorizontal="FILL";
807
+
808
+ // Left: artwork with component clone + markers (absolute positioning)
809
+ var artWrap=figma.createFrame();artWrap.name="Artwork";
810
+ artWrap.layoutMode="NONE";
811
+ artWrap.fills=[{type:"SOLID",color:${C.bgCard}}];artWrap.cornerRadius=8;
812
+ anatExhibit.appendChild(artWrap);
813
+
814
+ var src=sourceNode;
815
+ if(src.type==="COMPONENT_SET"&&src.children.length>0)src=src.children[0];
816
+ var clone=src.clone();
817
+ clone.x=${MARGIN};clone.y=${MARGIN};
818
+ ${scale > 1 ? `clone.rescale(${scale});` : ""}
819
+ artWrap.appendChild(clone);
820
+ var artW=clone.width+${MARGIN * 2}+40;
821
+ artWrap.resize(artW,clone.height+${MARGIN * 2});
822
+
823
+ var markers=${markersJSON};
824
+ var MD=${MD},MR=${MD / 2};
825
+
826
+ for(var i=0;i<markers.length;i++){
827
+ var mk=markers[i];
828
+
829
+ // Orange marker dot ON the element
830
+ var dot=figma.createEllipse();dot.resize(MD,MD);
831
+ dot.fills=[{type:"SOLID",color:${C.markerOrange}}];
832
+ dot.x=mk.markerX-MR;dot.y=mk.markerY-MR;dot.name="Marker "+(i+1);
833
+ artWrap.appendChild(dot);
834
+
835
+ // Number label centered on dot
836
+ var num=figma.createText();num.fontName=F.b;num.fontSize=11;
837
+ num.characters=String(i+1);
838
+ num.fills=[{type:"SOLID",color:{r:1,g:1,b:1}}];
839
+ num.textAutoResize="WIDTH_AND_HEIGHT";
840
+ artWrap.appendChild(num);
841
+ num.x=mk.markerX-num.width/2;num.y=mk.markerY-num.height/2;
842
+
843
+ // Dashed leader line from marker to right edge
844
+ var leaderStartX=mk.markerX+MR+2;
845
+ var leaderEndX=artW-8;
846
+ var leaderY=mk.markerY;
847
+ if(leaderEndX>leaderStartX+10){
848
+ var leader=figma.createVector();
849
+ leader.vectorPaths=[{windingRule:"NONZERO",data:"M "+leaderStartX+" "+leaderY+" L "+leaderEndX+" "+leaderY}];
850
+ leader.strokes=[{type:"SOLID",color:${C.markerOrange}}];
851
+ leader.strokeWeight=0.75;leader.fills=[];
852
+ leader.dashPattern=[3,3];leader.opacity=0.5;
853
+ leader.name="Leader "+(i+1);
854
+ artWrap.appendChild(leader);
855
+ }
856
+ }
857
+
858
+ // Right: anatomy content
859
+ var anatContent=figma.createFrame();anatContent.name="Anatomy Items";
860
+ anatContent.layoutMode="VERTICAL";
861
+ anatContent.primaryAxisSizingMode="AUTO";anatContent.counterAxisSizingMode="AUTO";
862
+ anatContent.itemSpacing=24;anatContent.fills=[];
863
+ anatExhibit.appendChild(anatContent);anatContent.layoutSizingHorizontal="FILL";
864
+
865
+ ${anatomyItemsJS}
866
+
867
+ return{ok:true};
868
+ })();
869
+ `);
870
+ if (!r.success) console.error("Anatomy exhibit failed:", r.error);
871
+ }
872
+
873
+ // ══════════════════════════════════════════════════════════════════════════
874
+ // PHASE 3: PROPERTY SECTIONS WITH GROUPED EXHIBITS
875
+ // ══════════════════════════════════════════════════════════════════════════
876
+
877
+ async function renderVariantsExhibit(
878
+ bridge: Bridge, mId: string, spec: ComponentSpec,
879
+ ): Promise<void> {
880
+ const snapshot = spec.extraction.snapshot;
881
+ const variantGroupProps = snapshot.variantGroupProperties || {};
882
+ const axisNames = Object.keys(variantGroupProps);
883
+ if (axisNames.length === 0) return;
884
+
885
+ const defaultProps = snapshot.variants?.[0]?.properties || {};
886
+ const MAX_PER_AXIS = 6;
887
+
888
+ // Create the section card first
889
+ const r0 = await bridge.execute(`
890
+ (async () => {
891
+ ${FL}
892
+ var m=await figma.getNodeByIdAsync(${esc(mId)});
893
+ if(!m)return{error:"no master"};
894
+ ${sectionCardOpenJS("varCard", "m", "Properties")}
895
+ ${sectionTitleJS("varTitle", "varCard", "Properties")}
896
+ return{ok:true,cardId:varCard.id};
897
+ })();
898
+ `);
899
+ if (!r0.success) { console.error("Variants card failed:", r0.error); return; }
900
+ const cardId = (r0.result as { cardId: string }).cardId;
901
+
902
+ // Render each axis as a subsection with exhibits
903
+ for (const axisName of axisNames) {
904
+ const values = variantGroupProps[axisName].slice(0, MAX_PER_AXIS);
905
+ const samples = values.map(val => ({
906
+ label: val,
907
+ props: { ...defaultProps, [axisName]: val },
908
+ }));
909
+
910
+ const r = await bridge.execute(`
911
+ (async () => {
912
+ ${FL}
913
+ ${EXTRACT_STYLE_FN}
914
+ var card=await figma.getNodeByIdAsync(${esc(cardId)});
915
+ if(!card)return{error:"no card"};
916
+ var sourceNode=await figma.getNodeByIdAsync(${esc(spec.nodeId)});
917
+ if(!sourceNode)return{error:"no source"};
918
+
919
+ // Axis group
920
+ var axGrp=figma.createFrame();axGrp.name=${esc(axisName + " Axis")};
921
+ axGrp.layoutMode="VERTICAL";
922
+ axGrp.primaryAxisSizingMode="AUTO";axGrp.counterAxisSizingMode="AUTO";
923
+ axGrp.itemSpacing=40;axGrp.fills=[];
924
+ card.appendChild(axGrp);axGrp.layoutSizingHorizontal="FILL";
925
+
926
+ // Axis title (36px bold)
927
+ ${subsectionTitleJS("axTitle", "axGrp", axisName)}
928
+
929
+ var compSet=sourceNode.type==="COMPONENT_SET"?sourceNode:(sourceNode.parent&&sourceNode.parent.type==="COMPONENT_SET"?sourceNode.parent:null);
930
+ if(!compSet)return{ok:true,skip:"not a component set"};
931
+
932
+ var samples=${JSON.stringify(samples)};
933
+
934
+ for(var si=0;si<samples.length;si++){
935
+ var s=samples[si];
936
+
937
+ // Find matching variant
938
+ var target=null;
939
+ for(var ci=0;ci<compSet.children.length;ci++){
940
+ var ch=compSet.children[ci];
941
+ if(!ch.variantProperties)continue;
942
+ var match=true;
943
+ for(var pk in s.props){if(ch.variantProperties[pk]!==s.props[pk]){match=false;break;}}
944
+ if(match){target=ch;break;}
945
+ }
946
+ if(!target&&compSet.children.length>0)target=compSet.children[0];
947
+ if(!target)continue;
948
+ var inst=target.createInstance();
949
+ var style=extractStyle(inst);
950
+
951
+ // Exhibit: artwork + content
952
+ var exh=figma.createFrame();exh.name=s.label;
953
+ exh.layoutMode="HORIZONTAL";
954
+ exh.primaryAxisSizingMode="AUTO";exh.counterAxisSizingMode="AUTO";
955
+ exh.itemSpacing=${EXHIBIT_GAP};exh.fills=[];
956
+ exh.counterAxisAlignItems="MIN";
957
+ axGrp.appendChild(exh);exh.layoutSizingHorizontal="FILL";
958
+
959
+ // Artwork (350x248 gray)
960
+ var art=figma.createFrame();art.name="Artwork";
961
+ art.layoutMode="HORIZONTAL";
962
+ art.primaryAxisSizingMode="FIXED";art.counterAxisSizingMode="FIXED";
963
+ art.resize(${ARTWORK_W},${ARTWORK_H});
964
+ art.fills=[{type:"SOLID",color:${C.bgCard}}];art.cornerRadius=8;
965
+ art.primaryAxisAlignItems="CENTER";art.counterAxisAlignItems="CENTER";
966
+ art.clipsContent=true;
967
+ exh.appendChild(art);
968
+ art.appendChild(inst);
969
+
970
+ // Content metadata
971
+ var cnt=figma.createFrame();cnt.name="Content";
972
+ cnt.layoutMode="VERTICAL";
973
+ cnt.primaryAxisSizingMode="AUTO";cnt.counterAxisSizingMode="AUTO";
974
+ cnt.itemSpacing=12;cnt.fills=[];
975
+ exh.appendChild(cnt);cnt.layoutSizingHorizontal="FILL";
976
+
977
+ // Option label (16px bold)
978
+ var optLbl=figma.createText();optLbl.fontName=F.b;optLbl.fontSize=16;
979
+ optLbl.lineHeight={unit:"PIXELS",value:22};
980
+ optLbl.characters=s.label;
981
+ optLbl.fills=[{type:"SOLID",color:${C.textMain}}];
982
+ optLbl.textAutoResize="HEIGHT";
983
+ cnt.appendChild(optLbl);optLbl.layoutSizingHorizontal="FILL";
984
+
985
+ // Style attributes
986
+ var attrCol=figma.createFrame();attrCol.name="Attributes";
987
+ attrCol.layoutMode="VERTICAL";
988
+ attrCol.primaryAxisSizingMode="AUTO";attrCol.counterAxisSizingMode="AUTO";
989
+ attrCol.itemSpacing=4;attrCol.fills=[];
990
+ cnt.appendChild(attrCol);attrCol.layoutSizingHorizontal="FILL";
991
+
992
+ // EightShapes-style: circle icon + element name, then indented attrs
993
+ function addElement(parent,name){
994
+ var elRow=figma.createFrame();elRow.name=name;
995
+ elRow.layoutMode="HORIZONTAL";
996
+ elRow.primaryAxisSizingMode="AUTO";elRow.counterAxisSizingMode="AUTO";
997
+ elRow.itemSpacing=6;elRow.fills=[];elRow.counterAxisAlignItems="CENTER";
998
+ parent.appendChild(elRow);elRow.layoutSizingHorizontal="FILL";
999
+ var ic=figma.createEllipse();ic.resize(10,10);
1000
+ ic.fills=[];ic.strokes=[{type:"SOLID",color:${C.textMuted}}];ic.strokeWeight=1;
1001
+ elRow.appendChild(ic);
1002
+ var nm=figma.createText();nm.fontName=F.sb;nm.fontSize=12;
1003
+ nm.lineHeight={unit:"PIXELS",value:18};nm.characters=name;
1004
+ nm.fills=[{type:"SOLID",color:${C.textMain}}];nm.textAutoResize="WIDTH_AND_HEIGHT";
1005
+ elRow.appendChild(nm);
1006
+ }
1007
+ function addAttr(parent,name,value){
1008
+ var row=figma.createFrame();row.name=name;
1009
+ row.layoutMode="HORIZONTAL";
1010
+ row.primaryAxisSizingMode="AUTO";row.counterAxisSizingMode="AUTO";
1011
+ row.itemSpacing=4;row.fills=[];row.paddingLeft=16;
1012
+ parent.appendChild(row);row.layoutSizingHorizontal="FILL";
1013
+ var n=figma.createText();n.fontName=F.r;n.fontSize=11;
1014
+ n.lineHeight={unit:"PIXELS",value:16};n.characters=name+":";
1015
+ n.fills=[{type:"SOLID",color:${C.textMuted}}];n.textAutoResize="WIDTH_AND_HEIGHT";
1016
+ n.resize(80,16);row.appendChild(n);n.layoutSizingHorizontal="FIXED";
1017
+ if(value&&value.charAt(0)==="#"&&value.length===7){
1018
+ var hex=value.replace("#","");
1019
+ var sw=figma.createRectangle();sw.resize(12,12);sw.cornerRadius=2;
1020
+ sw.fills=[{type:"SOLID",color:{
1021
+ r:parseInt(hex.substring(0,2),16)/255,
1022
+ g:parseInt(hex.substring(2,4),16)/255,
1023
+ b:parseInt(hex.substring(4,6),16)/255
1024
+ }}];sw.strokes=[{type:"SOLID",color:${C.border}}];sw.strokeWeight=0.5;
1025
+ row.appendChild(sw);
1026
+ }
1027
+ var v=figma.createText();v.fontName=F.sb;v.fontSize=11;
1028
+ v.lineHeight={unit:"PIXELS",value:16};v.characters=value;
1029
+ v.fills=[{type:"SOLID",color:${C.textMain}}];v.textAutoResize="WIDTH_AND_HEIGHT";
1030
+ row.appendChild(v);
1031
+ }
1032
+
1033
+ // Root element
1034
+ addElement(attrCol,inst.name||s.label);
1035
+ for(var fi=0;fi<Math.min(style.fills.length,3);fi++){
1036
+ addAttr(attrCol,"Fill",style.fills[fi]);
1037
+ }
1038
+ if(style.radius)addAttr(attrCol,"Radius",style.radius+"px");
1039
+ if(style.padding)addAttr(attrCol,"Padding",style.padding);
1040
+ addAttr(attrCol,"Size",style.w+" \\u00D7 "+style.h);
1041
+ // Child text elements as separate entries
1042
+ for(var ti=0;ti<Math.min(style.texts.length,4);ti++){
1043
+ var parts=style.texts[ti].split(" ");
1044
+ var tName=parts[0]||"Text";
1045
+ addElement(attrCol,tName);
1046
+ addAttr(attrCol,"Font",style.texts[ti]);
1047
+ }
1048
+ }
1049
+
1050
+ return{ok:true};
1051
+ })();
1052
+ `);
1053
+ if (!r.success) console.error(`Variants exhibit "${axisName}" failed:`, r.error);
1054
+ }
1055
+
1056
+ // Phase 6: Boolean toggles
1057
+ const boolToggles = (snapshot.componentProperties || []).filter(cp => cp.type === "BOOLEAN");
1058
+ for (const toggle of boolToggles) {
1059
+ const cleanName = toggle.name.replace(/#.*$/, "").trim();
1060
+ const r = await bridge.execute(`
1061
+ (async () => {
1062
+ ${FL}
1063
+ var card=await figma.getNodeByIdAsync(${esc(cardId)});
1064
+ if(!card)return{error:"no card"};
1065
+ var sourceNode=await figma.getNodeByIdAsync(${esc(spec.nodeId)});
1066
+ if(!sourceNode)return{error:"no source"};
1067
+
1068
+ // Bool group
1069
+ var boolGrp=figma.createFrame();boolGrp.name=${esc(cleanName + " (Boolean)")};
1070
+ boolGrp.layoutMode="VERTICAL";
1071
+ boolGrp.primaryAxisSizingMode="AUTO";boolGrp.counterAxisSizingMode="AUTO";
1072
+ boolGrp.itemSpacing=40;boolGrp.fills=[];
1073
+ card.appendChild(boolGrp);boolGrp.layoutSizingHorizontal="FILL";
1074
+
1075
+ ${subsectionTitleJS("boolTitle", "boolGrp", cleanName + " (Boolean)")}
1076
+
1077
+ var compSet=sourceNode.type==="COMPONENT_SET"?sourceNode:null;
1078
+ if(!compSet||compSet.children.length===0)return{ok:true};
1079
+ var base=compSet.children[0];
1080
+
1081
+ var propKey=null;
1082
+ if("componentProperties" in compSet&&compSet.componentProperties){
1083
+ for(var k in compSet.componentProperties){
1084
+ if(k.replace(/#.*$/,"").trim()===${esc(cleanName)}){propKey=k;break;}
1085
+ }
1086
+ }
1087
+
1088
+ // True/false exhibits side by side
1089
+ var boolRow=figma.createFrame();boolRow.name="Toggle Comparison";
1090
+ boolRow.layoutMode="HORIZONTAL";
1091
+ boolRow.primaryAxisSizingMode="AUTO";boolRow.counterAxisSizingMode="AUTO";
1092
+ boolRow.itemSpacing=24;boolRow.fills=[];
1093
+ boolGrp.appendChild(boolRow);boolRow.layoutSizingHorizontal="FILL";
1094
+
1095
+ var vals=[{v:true,label:"Visible (true)"},{v:false,label:"Hidden (false)"}];
1096
+ for(var vi=0;vi<vals.length;vi++){
1097
+ var inst=base.createInstance();
1098
+ if(propKey){try{var pp={};pp[propKey]=vals[vi].v;inst.setProperties(pp);}catch(e){}}
1099
+
1100
+ var bCard=figma.createFrame();bCard.name=${esc(cleanName)}+"="+vals[vi].v;
1101
+ bCard.layoutMode="VERTICAL";
1102
+ bCard.primaryAxisSizingMode="AUTO";bCard.counterAxisSizingMode="AUTO";
1103
+ bCard.itemSpacing=16;
1104
+ bCard.paddingTop=24;bCard.paddingBottom=24;bCard.paddingLeft=24;bCard.paddingRight=24;
1105
+ bCard.fills=[{type:"SOLID",color:${C.bgCard}}];bCard.cornerRadius=8;
1106
+ bCard.primaryAxisAlignItems="CENTER";
1107
+ boolRow.appendChild(bCard);bCard.layoutSizingHorizontal="FILL";
1108
+
1109
+ bCard.appendChild(inst);
1110
+
1111
+ var bLbl=figma.createText();bLbl.fontName=F.b;bLbl.fontSize=16;
1112
+ bLbl.characters=vals[vi].label;
1113
+ bLbl.fills=[{type:"SOLID",color:${C.textMain}}];
1114
+ bLbl.textAutoResize="WIDTH_AND_HEIGHT";
1115
+ bCard.appendChild(bLbl);
1116
+
1117
+ var bSub=figma.createText();bSub.fontName=F.r;bSub.fontSize=12;
1118
+ bSub.characters="Boolean layer";
1119
+ bSub.fills=[{type:"SOLID",color:${C.textMuted}}];
1120
+ bSub.textAutoResize="WIDTH_AND_HEIGHT";
1121
+ bCard.appendChild(bSub);
1122
+ }
1123
+
1124
+ return{ok:true};
1125
+ })();
1126
+ `);
1127
+ if (!r.success) console.error(`Boolean toggle "${cleanName}" failed:`, r.error);
1128
+ }
1129
+
1130
+ // Phase 6: Instance swaps
1131
+ const instSwaps = (snapshot.componentProperties || []).filter(cp => cp.type === "INSTANCE_SWAP");
1132
+ for (const swap of instSwaps) {
1133
+ const cleanName = swap.name.replace(/#.*$/, "").trim();
1134
+ const r = await bridge.execute(`
1135
+ (async () => {
1136
+ ${FL}
1137
+ var card=await figma.getNodeByIdAsync(${esc(cardId)});
1138
+ if(!card)return{error:"no card"};
1139
+
1140
+ var swapGrp=figma.createFrame();swapGrp.name=${esc(cleanName + " (Instance Swap)")};
1141
+ swapGrp.layoutMode="VERTICAL";
1142
+ swapGrp.primaryAxisSizingMode="AUTO";swapGrp.counterAxisSizingMode="AUTO";
1143
+ swapGrp.itemSpacing=16;swapGrp.fills=[];
1144
+ card.appendChild(swapGrp);swapGrp.layoutSizingHorizontal="FILL";
1145
+
1146
+ ${subsectionTitleJS("swapTitle", "swapGrp", cleanName + " (Instance Swap)")}
1147
+
1148
+ // Info card
1149
+ var swapInfo=figma.createFrame();swapInfo.name="Swap Info";
1150
+ swapInfo.layoutMode="HORIZONTAL";
1151
+ swapInfo.primaryAxisSizingMode="AUTO";swapInfo.counterAxisSizingMode="AUTO";
1152
+ swapInfo.itemSpacing=${EXHIBIT_GAP};swapInfo.fills=[];
1153
+ swapInfo.counterAxisAlignItems="MIN";
1154
+ swapGrp.appendChild(swapInfo);swapInfo.layoutSizingHorizontal="FILL";
1155
+
1156
+ var swapArt=figma.createFrame();swapArt.name="Artwork";
1157
+ swapArt.layoutMode="HORIZONTAL";
1158
+ swapArt.primaryAxisSizingMode="FIXED";swapArt.counterAxisSizingMode="FIXED";
1159
+ swapArt.resize(${ARTWORK_W},${ARTWORK_H / 2});
1160
+ swapArt.fills=[{type:"SOLID",color:${C.bgCard}}];swapArt.cornerRadius=8;
1161
+ swapArt.primaryAxisAlignItems="CENTER";swapArt.counterAxisAlignItems="CENTER";
1162
+ swapInfo.appendChild(swapArt);
1163
+
1164
+ // Placeholder text in artwork
1165
+ var phTxt=figma.createText();phTxt.fontName=F.r;phTxt.fontSize=14;
1166
+ phTxt.characters="Instance Swap: "+${esc(cleanName)};
1167
+ phTxt.fills=[{type:"SOLID",color:${C.textMuted}}];
1168
+ phTxt.textAutoResize="WIDTH_AND_HEIGHT";
1169
+ swapArt.appendChild(phTxt);
1170
+
1171
+ var swapCnt=figma.createFrame();swapCnt.name="Content";
1172
+ swapCnt.layoutMode="VERTICAL";
1173
+ swapCnt.primaryAxisSizingMode="AUTO";swapCnt.counterAxisSizingMode="AUTO";
1174
+ swapCnt.itemSpacing=8;swapCnt.fills=[];
1175
+ swapInfo.appendChild(swapCnt);swapCnt.layoutSizingHorizontal="FILL";
1176
+
1177
+ var swapLbl=figma.createText();swapLbl.fontName=F.b;swapLbl.fontSize=20;
1178
+ swapLbl.characters=${esc(cleanName)};
1179
+ swapLbl.fills=[{type:"SOLID",color:${C.textMain}}];
1180
+ swapLbl.textAutoResize="WIDTH_AND_HEIGHT";
1181
+ swapCnt.appendChild(swapLbl);
1182
+
1183
+ var swapVal=figma.createText();swapVal.fontName=F.r;swapVal.fontSize=14;
1184
+ swapVal.characters="Current: "+${esc(swap.value || "Default")};
1185
+ swapVal.fills=[{type:"SOLID",color:${C.textMuted}}];
1186
+ swapVal.textAutoResize="WIDTH_AND_HEIGHT";
1187
+ swapCnt.appendChild(swapVal);
1188
+
1189
+ var swapType=figma.createText();swapType.fontName=F.r;swapType.fontSize=12;
1190
+ swapType.characters="Type: Instance Swap";
1191
+ swapType.fills=[{type:"SOLID",color:${C.textMuted}}];
1192
+ swapType.textAutoResize="WIDTH_AND_HEIGHT";
1193
+ swapCnt.appendChild(swapType);
1194
+
1195
+ return{ok:true};
1196
+ })();
1197
+ `);
1198
+ if (!r.success) console.error(`Instance swap "${cleanName}" failed:`, r.error);
1199
+ }
1200
+ }
1201
+
1202
+ // ══════════════════════════════════════════════════════════════════════════
1203
+ // PHASE 3b: COMPACT PROPERTY TABLE — Summary table + variant strips
1204
+ // ══════════════════════════════════════════════════════════════════════════
1205
+
1206
+ async function renderPropertyTable(
1207
+ bridge: Bridge, mId: string, spec: ComponentSpec, sectionNum?: number,
1208
+ ): Promise<void> {
1209
+ const snapshot = spec.extraction.snapshot;
1210
+ const variantGroupProps = snapshot.variantGroupProperties || {};
1211
+ const axisNames = Object.keys(variantGroupProps);
1212
+ const boolToggles = (snapshot.componentProperties || []).filter(cp => cp.type === "BOOLEAN");
1213
+ const instSwaps = (snapshot.componentProperties || []).filter(cp => cp.type === "INSTANCE_SWAP");
1214
+
1215
+ if (axisNames.length === 0 && boolToggles.length === 0 && instSwaps.length === 0) return;
1216
+
1217
+ // Build property summary rows
1218
+ const propRows: Array<[string, string, string, string]> = [];
1219
+ for (const axis of axisNames) {
1220
+ const vals = variantGroupProps[axis];
1221
+ propRows.push([axis, "Variant", vals[0] || "\u2014", vals.slice(0, 8).join(", ")]);
1222
+ }
1223
+ for (const b of boolToggles) {
1224
+ const name = b.name.replace(/#.*$/, "").trim();
1225
+ propRows.push([name, "Boolean", String(b.value), "true, false"]);
1226
+ }
1227
+ for (const s of instSwaps) {
1228
+ const name = s.name.replace(/#.*$/, "").trim();
1229
+ propRows.push([name, "Instance Swap", s.value || "Default", "\u2014"]);
1230
+ }
1231
+
1232
+ // Build all variant strips + boolean toggles JS to embed in single call
1233
+ const defaultProps = snapshot.variants?.[0]?.properties || {};
1234
+ const MAX_PER_AXIS = 6;
1235
+ const compW = snapshot.width || 200;
1236
+ const compH = snapshot.height || 60;
1237
+ // 3-column grid: card width = (content area - 2 gaps) / 3
1238
+ const GRID_GAP = 16;
1239
+ const GRID_COLS = 3;
1240
+ const cardInnerW = Math.floor((CONTENT_W - CARD_PAD * 2 - GRID_GAP * (GRID_COLS - 1)) / GRID_COLS);
1241
+ const thumbScale = Math.min(2, Math.max(0.8, (cardInnerW - 32) / Math.max(compW, 1)));
1242
+ const thumbW = cardInnerW - 32; // artwork width = card width minus padding
1243
+ const thumbH = Math.max(48, Math.min(120, Math.round(compH * thumbScale) + 16));
1244
+
1245
+ // Build JS for all axis strips — 3-column card grid with style metadata
1246
+ const allAxesJS = axisNames.map((axisName, ai) => {
1247
+ const values = variantGroupProps[axisName].slice(0, MAX_PER_AXIS);
1248
+ const remaining = variantGroupProps[axisName].length - values.length;
1249
+ const samples = values.map(val => ({
1250
+ label: val,
1251
+ props: { ...defaultProps, [axisName]: val },
1252
+ }));
1253
+ return `
1254
+ // ── Axis: ${axisName} ──
1255
+ ${subsectionTitleJS(`axT${ai}`, "propCard", axisName)}
1256
+ var grid${ai}=figma.createFrame();grid${ai}.name=${esc(axisName + " Grid")};
1257
+ grid${ai}.layoutMode="HORIZONTAL";grid${ai}.layoutWrap="WRAP";
1258
+ grid${ai}.primaryAxisSizingMode="AUTO";grid${ai}.counterAxisSizingMode="AUTO";
1259
+ grid${ai}.itemSpacing=${GRID_GAP};grid${ai}.counterAxisSpacing=${GRID_GAP};grid${ai}.fills=[];
1260
+ propCard.appendChild(grid${ai});grid${ai}.layoutSizingHorizontal="FILL";
1261
+
1262
+ ${EXTRACT_STYLE_FN}
1263
+
1264
+ if(compSet){
1265
+ var samples${ai}=${JSON.stringify(samples)};
1266
+ for(var si=0;si<samples${ai}.length;si++){
1267
+ var s=samples${ai}[si];
1268
+ var target=null;
1269
+ for(var ci=0;ci<compSet.children.length;ci++){
1270
+ var ch=compSet.children[ci];
1271
+ if(!ch.variantProperties)continue;
1272
+ var match=true;
1273
+ for(var pk in s.props){if(ch.variantProperties[pk]!==s.props[pk]){match=false;break;}}
1274
+ if(match){target=ch;break;}
1275
+ }
1276
+ if(!target&&compSet.children.length>0)target=compSet.children[0];
1277
+ if(!target)continue;
1278
+
1279
+ // Fixed-width card for 3-column grid
1280
+ var card${ai}=figma.createFrame();card${ai}.name=s.label;
1281
+ card${ai}.layoutMode="VERTICAL";
1282
+ card${ai}.primaryAxisSizingMode="AUTO";card${ai}.counterAxisSizingMode="FIXED";
1283
+ card${ai}.resize(${cardInnerW},10);
1284
+ card${ai}.itemSpacing=12;
1285
+ card${ai}.paddingTop=16;card${ai}.paddingBottom=16;card${ai}.paddingLeft=16;card${ai}.paddingRight=16;
1286
+ card${ai}.fills=[{type:"SOLID",color:${C.bgCard}}];card${ai}.cornerRadius=8;
1287
+ grid${ai}.appendChild(card${ai});
1288
+
1289
+ // Artwork container — centered
1290
+ var art=figma.createFrame();art.name="Artwork";
1291
+ art.layoutMode="HORIZONTAL";
1292
+ art.primaryAxisSizingMode="FIXED";art.counterAxisSizingMode="FIXED";
1293
+ art.resize(${thumbW},${thumbH});
1294
+ art.fills=[];art.cornerRadius=4;
1295
+ art.primaryAxisAlignItems="CENTER";art.counterAxisAlignItems="CENTER";
1296
+ art.clipsContent=true;
1297
+ card${ai}.appendChild(art);art.layoutSizingHorizontal="FILL";
1298
+ var inst=target.createInstance();
1299
+ art.appendChild(inst);
1300
+
1301
+ // Bold label
1302
+ var lbl=figma.createText();lbl.fontName=F.b;lbl.fontSize=14;
1303
+ lbl.lineHeight={unit:"PIXELS",value:20};lbl.characters=s.label;
1304
+ lbl.fills=[{type:"SOLID",color:${C.textMain}}];
1305
+ lbl.textAutoResize="HEIGHT";
1306
+ card${ai}.appendChild(lbl);lbl.layoutSizingHorizontal="FILL";
1307
+
1308
+ // Style metadata from instance
1309
+ var sInfo=extractStyle(inst);
1310
+ var metaParts=[];
1311
+ if(sInfo.fills.length>0)metaParts.push("Fill: "+sInfo.fills[0]);
1312
+ if(sInfo.radius)metaParts.push("Radius: "+sInfo.radius+"px");
1313
+ if(sInfo.padding)metaParts.push("Padding: "+sInfo.padding);
1314
+ if(sInfo.w&&sInfo.h)metaParts.push(sInfo.w+"\\u00D7"+sInfo.h);
1315
+ if(sInfo.texts.length>0)metaParts.push(sInfo.texts[0]);
1316
+ var metaStr=metaParts.slice(0,3).join(" \\u00B7 ");
1317
+ if(metaStr){
1318
+ var meta=figma.createText();meta.fontName=F.mono;meta.fontSize=11;
1319
+ meta.lineHeight={unit:"PIXELS",value:16};meta.characters=metaStr;
1320
+ meta.fills=[{type:"SOLID",color:${C.textMuted}}];
1321
+ meta.textAutoResize="HEIGHT";
1322
+ card${ai}.appendChild(meta);meta.layoutSizingHorizontal="FILL";
1323
+ }
1324
+ }
1325
+ ${remaining > 0 ? `
1326
+ // "and N more" label
1327
+ var moreLabel${ai}=figma.createText();moreLabel${ai}.fontName=F.r;moreLabel${ai}.fontSize=13;
1328
+ moreLabel${ai}.lineHeight={unit:"PIXELS",value:20};
1329
+ moreLabel${ai}.characters="and ${remaining} more value${remaining > 1 ? "s" : ""}\\u2026";
1330
+ moreLabel${ai}.fills=[{type:"SOLID",color:${C.textMuted}}];
1331
+ moreLabel${ai}.textAutoResize="WIDTH_AND_HEIGHT";
1332
+ propCard.appendChild(moreLabel${ai});
1333
+ ` : ""}
1334
+ }`;
1335
+ }).join("\n");
1336
+
1337
+ // Build JS for boolean toggles
1338
+ const allBoolsJS = boolToggles.map((toggle, bi) => {
1339
+ const cleanName = toggle.name.replace(/#.*$/, "").trim();
1340
+ return `
1341
+ // ── Boolean: ${cleanName} ──
1342
+ ${subsectionTitleJS(`boolT${bi}`, "propCard", cleanName + " (Boolean)")}
1343
+ if(compSet&&compSet.children.length>0){
1344
+ var base${bi}=compSet.children[0];
1345
+ var propKey${bi}=null;
1346
+ if("componentProperties" in compSet&&compSet.componentProperties){
1347
+ for(var k in compSet.componentProperties){
1348
+ if(k.replace(/#.*$/,"").trim()===${esc(cleanName)}){propKey${bi}=k;break;}
1349
+ }
1350
+ }
1351
+ var boolRow${bi}=figma.createFrame();boolRow${bi}.name="Toggle Comparison";
1352
+ boolRow${bi}.layoutMode="HORIZONTAL";
1353
+ boolRow${bi}.primaryAxisSizingMode="AUTO";boolRow${bi}.counterAxisSizingMode="AUTO";
1354
+ boolRow${bi}.itemSpacing=16;boolRow${bi}.fills=[];
1355
+ propCard.appendChild(boolRow${bi});boolRow${bi}.layoutSizingHorizontal="FILL";
1356
+ var bvals${bi}=[{v:true,label:"true"},{v:false,label:"false"}];
1357
+ for(var vi=0;vi<bvals${bi}.length;vi++){
1358
+ var binst=base${bi}.createInstance();
1359
+ if(propKey${bi}){try{var pp={};pp[propKey${bi}]=bvals${bi}[vi].v;binst.setProperties(pp);}catch(e){}}
1360
+ var bc=figma.createFrame();bc.name=${esc(cleanName)}+"="+bvals${bi}[vi].v;
1361
+ bc.layoutMode="VERTICAL";
1362
+ bc.primaryAxisSizingMode="AUTO";bc.counterAxisSizingMode="AUTO";
1363
+ bc.itemSpacing=8;
1364
+ bc.paddingTop=16;bc.paddingBottom=16;bc.paddingLeft=16;bc.paddingRight=16;
1365
+ bc.fills=[{type:"SOLID",color:${C.bgCard}}];bc.cornerRadius=8;
1366
+ bc.primaryAxisAlignItems="CENTER";
1367
+ boolRow${bi}.appendChild(bc);bc.layoutSizingHorizontal="FILL";
1368
+ bc.appendChild(binst);
1369
+ var blbl=figma.createText();blbl.fontName=F.sb;blbl.fontSize=12;
1370
+ blbl.characters=bvals${bi}[vi].label;
1371
+ blbl.fills=[{type:"SOLID",color:${C.textMain}}];
1372
+ blbl.textAutoResize="WIDTH_AND_HEIGHT";
1373
+ bc.appendChild(blbl);
1374
+ }
1375
+ }`;
1376
+ }).join("\n");
1377
+
1378
+ // Single consolidated call: table + all strips + all booleans
1379
+ const propRowsJSON = JSON.stringify(propRows);
1380
+ const r0 = await bridge.execute(`
1381
+ (async () => {
1382
+ ${FL}
1383
+ var m=await figma.getNodeByIdAsync(${esc(mId)});
1384
+ if(!m)return{error:"no master"};
1385
+ var sourceNode=await figma.getNodeByIdAsync(${esc(spec.nodeId)});
1386
+ if(!sourceNode)return{error:"no source"};
1387
+
1388
+ ${sectionCardOpenJS("propCard", "m", "Properties")}
1389
+ ${sectionTitleJS("propTitle", "propCard", "Properties", sectionNum)}
1390
+
1391
+ // ── Property summary table ──
1392
+ var headers=["Property","Type","Default","Options"];
1393
+ var rows=${propRowsJSON};
1394
+ var colWidths=[180,120,140,0];
1395
+
1396
+ var tbl=figma.createFrame();tbl.name="Property Table";
1397
+ tbl.layoutMode="VERTICAL";
1398
+ tbl.primaryAxisSizingMode="AUTO";tbl.counterAxisSizingMode="AUTO";
1399
+ tbl.itemSpacing=0;tbl.fills=[{type:"SOLID",color:${C.white}}];
1400
+ tbl.cornerRadius=8;
1401
+ tbl.strokes=[{type:"SOLID",color:${C.border}}];tbl.strokeWeight=1;tbl.strokeAlign="INSIDE";
1402
+ tbl.clipsContent=true;
1403
+ propCard.appendChild(tbl);tbl.layoutSizingHorizontal="FILL";
1404
+
1405
+ var hdr=figma.createFrame();hdr.name="Header";
1406
+ hdr.layoutMode="HORIZONTAL";
1407
+ hdr.primaryAxisSizingMode="AUTO";hdr.counterAxisSizingMode="AUTO";
1408
+ hdr.itemSpacing=0;hdr.paddingTop=12;hdr.paddingBottom=12;
1409
+ hdr.paddingLeft=20;hdr.paddingRight=20;
1410
+ hdr.fills=[{type:"SOLID",color:${C.bgCard}}];hdr.counterAxisAlignItems="CENTER";
1411
+ hdr.strokes=[{type:"SOLID",color:${C.border}}];hdr.strokeWeight=1;
1412
+ hdr.strokeTopWeight=0;hdr.strokeRightWeight=0;hdr.strokeBottomWeight=1;hdr.strokeLeftWeight=0;
1413
+ hdr.strokeAlign="INSIDE";
1414
+ tbl.appendChild(hdr);hdr.layoutSizingHorizontal="FILL";
1415
+
1416
+ for(var hi=0;hi<headers.length;hi++){
1417
+ var ht=figma.createText();ht.fontName=F.sb;ht.fontSize=13;
1418
+ ht.lineHeight={unit:"PIXELS",value:20};ht.characters=headers[hi];
1419
+ ht.fills=[{type:"SOLID",color:${C.textMain}}];ht.textAutoResize="HEIGHT";
1420
+ hdr.appendChild(ht);
1421
+ if(colWidths[hi]>0){ht.resize(colWidths[hi],20);ht.layoutSizingHorizontal="FIXED";}
1422
+ else{ht.layoutSizingHorizontal="FILL";}
1423
+ }
1424
+
1425
+ for(var ri=0;ri<rows.length;ri++){
1426
+ var row=figma.createFrame();row.name=rows[ri][0];
1427
+ row.layoutMode="HORIZONTAL";
1428
+ row.primaryAxisSizingMode="AUTO";row.counterAxisSizingMode="AUTO";
1429
+ row.itemSpacing=0;row.paddingTop=10;row.paddingBottom=10;
1430
+ row.paddingLeft=20;row.paddingRight=20;
1431
+ row.fills=[{type:"SOLID",color:ri%2===0?${C.white}:${C.bgCard}}];
1432
+ row.counterAxisAlignItems="CENTER";
1433
+ if(ri<rows.length-1){
1434
+ row.strokes=[{type:"SOLID",color:${C.border}}];row.strokeWeight=1;
1435
+ row.strokeTopWeight=0;row.strokeRightWeight=0;row.strokeBottomWeight=1;row.strokeLeftWeight=0;
1436
+ row.strokeAlign="INSIDE";
1437
+ }
1438
+ tbl.appendChild(row);row.layoutSizingHorizontal="FILL";
1439
+ for(var ci=0;ci<4;ci++){
1440
+ var ct=figma.createText();
1441
+ ct.fontName=ci===3?F.mono:(ci===0?F.sb:F.r);
1442
+ ct.fontSize=ci===3?11:13;
1443
+ ct.lineHeight={unit:"PIXELS",value:ci===3?16:20};
1444
+ ct.characters=rows[ri][ci]||"\\u2014";
1445
+ ct.fills=[{type:"SOLID",color:ci===0?${C.textMain}:${C.textMuted}}];
1446
+ ct.textAutoResize="HEIGHT";
1447
+ row.appendChild(ct);
1448
+ if(colWidths[ci]>0){ct.resize(colWidths[ci],20);ct.layoutSizingHorizontal="FIXED";}
1449
+ else{ct.layoutSizingHorizontal="FILL";}
1450
+ }
1451
+ }
1452
+
1453
+ // ── Variant strips + booleans ──
1454
+ var compSet=sourceNode.type==="COMPONENT_SET"?sourceNode:(sourceNode.parent&&sourceNode.parent.type==="COMPONENT_SET"?sourceNode.parent:null);
1455
+
1456
+ ${allAxesJS}
1457
+ ${allBoolsJS}
1458
+
1459
+ return{ok:true};
1460
+ })();
1461
+ `);
1462
+ if (!r0.success) console.error("Property table failed:", r0.error);
1463
+ }
1464
+
1465
+ // ══════════════════════════════════════════════════════════════════════════
1466
+ // PHASE 4b: SIZING COMPARISON — Side-by-side size variants with dimensions
1467
+ // ══════════════════════════════════════════════════════════════════════════
1468
+
1469
+ async function renderSizingComparison(
1470
+ bridge: Bridge, mId: string, spec: ComponentSpec, sectionNum?: number,
1471
+ ): Promise<void> {
1472
+ const snapshot = spec.extraction.snapshot;
1473
+ const variantGroupProps = snapshot.variantGroupProperties || {};
1474
+ // Find a "Size" axis
1475
+ const sizeAxis = Object.keys(variantGroupProps).find(k => /^size$/i.test(k));
1476
+ if (!sizeAxis) return;
1477
+
1478
+ const sizeValues = variantGroupProps[sizeAxis];
1479
+ if (sizeValues.length < 2) return;
1480
+
1481
+ const defaultProps = snapshot.variants?.[0]?.properties || {};
1482
+ const spacing = spec.extraction.spacing;
1483
+ const root = spacing.length > 0 ? spacing[0] : null;
1484
+ const LINE_W = 1.5;
1485
+
1486
+ // Build samples for each size value
1487
+ const samples = sizeValues.slice(0, 5).map(val => ({
1488
+ label: val,
1489
+ props: { ...defaultProps, [sizeAxis]: val },
1490
+ }));
1491
+ const samplesJSON = JSON.stringify(samples);
1492
+
1493
+ const r = await bridge.execute(`
1494
+ (async () => {
1495
+ ${FL}
1496
+ var m=await figma.getNodeByIdAsync(${esc(mId)});
1497
+ if(!m)return{error:"no master"};
1498
+ var sourceNode=await figma.getNodeByIdAsync(${esc(spec.nodeId)});
1499
+ if(!sourceNode)return{error:"no source"};
1500
+
1501
+ ${sectionCardOpenJS("szCard", "m", "Spacing & Dimensions")}
1502
+ ${sectionTitleJS("szTitle", "szCard", "Spacing & Dimensions", sectionNum)}
1503
+
1504
+ var compSet=sourceNode.type==="COMPONENT_SET"?sourceNode:(sourceNode.parent&&sourceNode.parent.type==="COMPONENT_SET"?sourceNode.parent:null);
1505
+ if(!compSet)return{ok:true};
1506
+
1507
+ // Horizontal row of size variants, bottom-aligned
1508
+ var szRow=figma.createFrame();szRow.name="Size Comparison";
1509
+ szRow.layoutMode="HORIZONTAL";
1510
+ szRow.primaryAxisSizingMode="AUTO";szRow.counterAxisSizingMode="AUTO";
1511
+ szRow.itemSpacing=32;szRow.fills=[];
1512
+ szRow.counterAxisAlignItems="MAX";
1513
+ szCard.appendChild(szRow);szRow.layoutSizingHorizontal="FILL";
1514
+
1515
+ var samples=${samplesJSON};
1516
+ var sizeColor=${C.sizeBlue};
1517
+ var padColor=${C.padGreen};
1518
+
1519
+ ${EXTRACT_STYLE_FN}
1520
+
1521
+ for(var si=0;si<samples.length;si++){
1522
+ var s=samples[si];
1523
+ var target=null;
1524
+ for(var ci=0;ci<compSet.children.length;ci++){
1525
+ var ch=compSet.children[ci];
1526
+ if(!ch.variantProperties)continue;
1527
+ var match=true;
1528
+ for(var pk in s.props){if(ch.variantProperties[pk]!==s.props[pk]){match=false;break;}}
1529
+ if(match){target=ch;break;}
1530
+ }
1531
+ if(!target)continue;
1532
+
1533
+ // Column for this size variant
1534
+ var col=figma.createFrame();col.name=s.label;
1535
+ col.layoutMode="VERTICAL";
1536
+ col.primaryAxisSizingMode="AUTO";col.counterAxisSizingMode="AUTO";
1537
+ col.itemSpacing=0;col.fills=[];
1538
+ col.primaryAxisAlignItems="CENTER";
1539
+ szRow.appendChild(col);col.layoutSizingHorizontal="FILL";
1540
+
1541
+ // Size label header
1542
+ var szLbl=figma.createText();szLbl.fontName=F.b;szLbl.fontSize=14;
1543
+ szLbl.lineHeight={unit:"PIXELS",value:20};
1544
+ szLbl.characters=s.label;
1545
+ szLbl.fills=[{type:"SOLID",color:${C.textMain}}];
1546
+ szLbl.textAutoResize="WIDTH_AND_HEIGHT";
1547
+ col.appendChild(szLbl);
1548
+
1549
+ // Spacer
1550
+ var sp1=figma.createFrame();sp1.resize(4,16);sp1.fills=[];
1551
+ col.appendChild(sp1);sp1.layoutSizingHorizontal="FILL";
1552
+
1553
+ // Canvas with component + dimension annotations
1554
+ var inst=target.createInstance();
1555
+ var iW=inst.width,iH=inst.height;
1556
+ var margin=40;
1557
+
1558
+ var canvas=figma.createFrame();canvas.name=s.label+" Canvas";
1559
+ canvas.layoutMode="NONE";
1560
+ canvas.fills=[{type:"SOLID",color:${C.bgCard}}];canvas.cornerRadius=8;
1561
+ canvas.resize(iW+margin*2,iH+margin*2);
1562
+ col.appendChild(canvas);
1563
+
1564
+ inst.x=margin;inst.y=margin;
1565
+ canvas.appendChild(inst);
1566
+
1567
+ // Blue outline
1568
+ var outline=figma.createRectangle();
1569
+ outline.resize(iW,iH);outline.x=margin;outline.y=margin;outline.fills=[];
1570
+ outline.strokes=[{type:"SOLID",color:sizeColor}];outline.strokeWeight=1;outline.strokeAlign="OUTSIDE";
1571
+ canvas.appendChild(outline);
1572
+
1573
+ // Width dimension line (above)
1574
+ function dimLine(parent,x1,y1,x2,y2,label,color){
1575
+ var isH=Math.abs(y2-y1)<Math.abs(x2-x1);
1576
+ var line=figma.createVector();
1577
+ line.vectorPaths=[{windingRule:"NONZERO",data:"M "+x1+" "+y1+" L "+x2+" "+y2}];
1578
+ line.strokes=[{type:"SOLID",color:color}];line.strokeWeight=${LINE_W};line.fills=[];
1579
+ line.strokeCap="ROUND";parent.appendChild(line);
1580
+ var cap=4;
1581
+ if(isH){
1582
+ var c1=figma.createVector();c1.vectorPaths=[{windingRule:"NONZERO",data:"M "+x1+" "+(y1-cap)+" L "+x1+" "+(y1+cap)}];
1583
+ c1.strokes=[{type:"SOLID",color:color}];c1.strokeWeight=${LINE_W};c1.fills=[];parent.appendChild(c1);
1584
+ var c2=figma.createVector();c2.vectorPaths=[{windingRule:"NONZERO",data:"M "+x2+" "+(y2-cap)+" L "+x2+" "+(y2+cap)}];
1585
+ c2.strokes=[{type:"SOLID",color:color}];c2.strokeWeight=${LINE_W};c2.fills=[];parent.appendChild(c2);
1586
+ }else{
1587
+ var c1=figma.createVector();c1.vectorPaths=[{windingRule:"NONZERO",data:"M "+(x1-cap)+" "+y1+" L "+(x1+cap)+" "+y1}];
1588
+ c1.strokes=[{type:"SOLID",color:color}];c1.strokeWeight=${LINE_W};c1.fills=[];parent.appendChild(c1);
1589
+ var c2=figma.createVector();c2.vectorPaths=[{windingRule:"NONZERO",data:"M "+(x2-cap)+" "+y2+" L "+(x2+cap)+" "+y2}];
1590
+ c2.strokes=[{type:"SOLID",color:color}];c2.strokeWeight=${LINE_W};c2.fills=[];parent.appendChild(c2);
1591
+ }
1592
+ var lbl=figma.createText();lbl.fontName=F.mono;lbl.fontSize=10;
1593
+ lbl.characters=label;lbl.fills=[{type:"SOLID",color:color}];
1594
+ lbl.textAutoResize="WIDTH_AND_HEIGHT";parent.appendChild(lbl);
1595
+ var mx=(x1+x2)/2,my=(y1+y2)/2;
1596
+ if(isH){lbl.x=mx-lbl.width/2;lbl.y=my-lbl.height-4;}
1597
+ else{lbl.x=mx+4;lbl.y=my-lbl.height/2;}
1598
+ }
1599
+
1600
+ dimLine(canvas,margin,margin-14,margin+iW,margin-14,Math.round(iW)+"px",sizeColor);
1601
+ dimLine(canvas,margin-14,margin,margin-14,margin+iH,Math.round(iH)+"px",sizeColor);
1602
+
1603
+ // Spacer before metadata
1604
+ var sp2=figma.createFrame();sp2.resize(4,12);sp2.fills=[];
1605
+ col.appendChild(sp2);sp2.layoutSizingHorizontal="FILL";
1606
+
1607
+ // Extract and show key dimensions
1608
+ var info=extractStyle(inst);
1609
+ var dimParts=[];
1610
+ dimParts.push("Height: "+Math.round(iH)+"px");
1611
+ dimParts.push("Width: "+Math.round(iW)+"px");
1612
+ if(info.padding)dimParts.push("Padding: "+info.padding);
1613
+ if(info.radius)dimParts.push("Radius: "+info.radius+"px");
1614
+
1615
+ for(var di=0;di<dimParts.length;di++){
1616
+ var dTxt=figma.createText();dTxt.fontName=F.mono;dTxt.fontSize=11;
1617
+ dTxt.lineHeight={unit:"PIXELS",value:18};
1618
+ dTxt.characters=dimParts[di];
1619
+ dTxt.fills=[{type:"SOLID",color:${C.textMuted}}];
1620
+ dTxt.textAutoResize="HEIGHT";
1621
+ col.appendChild(dTxt);dTxt.layoutSizingHorizontal="FILL";
1622
+ }
1623
+ }
1624
+
1625
+ return{ok:true};
1626
+ })();
1627
+ `);
1628
+ if (!r.success) console.error("Sizing comparison failed:", r.error);
1629
+ }
1630
+
1631
+ // ══════════════════════════════════════════════════════════════════════════
1632
+ // PHASE 5: SPACING EXHIBIT — Green/orange/blue overlays + content panel
1633
+ // ══════════════════════════════════════════════════════════════════════════
1634
+
1635
+ async function renderSpacingExhibit(
1636
+ bridge: Bridge, mId: string, spec: ComponentSpec, sectionNum?: number,
1637
+ ): Promise<void> {
1638
+ const spacing = spec.extraction.spacing;
1639
+ if (spacing.length === 0) return;
1640
+
1641
+ const root = spacing[0];
1642
+ const childrenData = JSON.stringify(root.children || []);
1643
+ const LINE_W = 1.5;
1644
+
1645
+ const r = await bridge.execute(`
1646
+ (async () => {
1647
+ ${FL}
1648
+ var m=await figma.getNodeByIdAsync(${esc(mId)});
1649
+ if(!m)return{error:"no master"};
1650
+ var sourceNode=await figma.getNodeByIdAsync(${esc(spec.nodeId)});
1651
+ if(!sourceNode)return{error:"no source"};
1652
+
1653
+ // Section card
1654
+ ${sectionCardOpenJS("spcCard", "m", "Layout & Spacing")}
1655
+ ${sectionTitleJS("spcTitle", "spcCard", "Layout & Spacing", sectionNum)}
1656
+
1657
+ // Exhibit: artwork left | content right
1658
+ var spcExh=figma.createFrame();spcExh.name="Spacing Exhibit";
1659
+ spcExh.layoutMode="HORIZONTAL";
1660
+ spcExh.primaryAxisSizingMode="AUTO";spcExh.counterAxisSizingMode="AUTO";
1661
+ spcExh.itemSpacing=${EXHIBIT_GAP};spcExh.fills=[];
1662
+ spcExh.counterAxisAlignItems="MIN";
1663
+ spcCard.appendChild(spcExh);spcExh.layoutSizingHorizontal="FILL";
1664
+
1665
+ // ── Left: Artwork with overlays (absolute positioning) ──
1666
+ var src=sourceNode;
1667
+ if(src.type==="COMPONENT_SET"&&src.children.length>0)src=src.children[0];
1668
+ var compClone=src.clone();
1669
+ var cW=compClone.width,cH=compClone.height;
1670
+ var AM=80;
1671
+
1672
+ var canvas=figma.createFrame();canvas.name="Spacing Artwork";
1673
+ canvas.layoutMode="NONE";
1674
+ canvas.fills=[{type:"SOLID",color:${C.bgCard}}];canvas.cornerRadius=8;
1675
+ canvas.resize(Math.max(cW+AM*2,500),cH+AM*2);
1676
+ spcExh.appendChild(canvas);
1677
+
1678
+ compClone.x=AM;compClone.y=AM;
1679
+ canvas.appendChild(compClone);
1680
+
1681
+ // Blue outline around component
1682
+ var blueOutline=figma.createRectangle();
1683
+ blueOutline.resize(cW,cH);blueOutline.x=AM;blueOutline.y=AM;
1684
+ blueOutline.fills=[];
1685
+ blueOutline.strokes=[{type:"SOLID",color:${C.sizeBlue}}];
1686
+ blueOutline.strokeWeight=1;blueOutline.strokeAlign="OUTSIDE";
1687
+ canvas.appendChild(blueOutline);
1688
+
1689
+ var padColor=${C.padGreen};
1690
+ var gapColor=${C.gapOrange};
1691
+ var sizeColor=${C.sizeBlue};
1692
+ var pt=${root.paddingTop},pr=${root.paddingRight},pb=${root.paddingBottom},pl=${root.paddingLeft};
1693
+ var compX=AM,compY=AM;
1694
+
1695
+ // ── Green padding: L-shaped corner brackets ──
1696
+ function drawBracket(parent,x,y,orient,armLen,color){
1697
+ var hPath,vPath;
1698
+ if(orient==="TL"){hPath="M "+x+" "+y+" L "+(x+armLen)+" "+y;vPath="M "+x+" "+y+" L "+x+" "+(y+armLen);}
1699
+ else if(orient==="TR"){hPath="M "+x+" "+y+" L "+(x-armLen)+" "+y;vPath="M "+x+" "+y+" L "+x+" "+(y+armLen);}
1700
+ else if(orient==="BL"){hPath="M "+x+" "+y+" L "+(x+armLen)+" "+y;vPath="M "+x+" "+y+" L "+x+" "+(y-armLen);}
1701
+ else{hPath="M "+x+" "+y+" L "+(x-armLen)+" "+y;vPath="M "+x+" "+y+" L "+x+" "+(y-armLen);}
1702
+ var h=figma.createVector();h.vectorPaths=[{windingRule:"NONZERO",data:hPath}];
1703
+ h.strokes=[{type:"SOLID",color:color}];h.strokeWeight=1.5;h.fills=[];h.strokeCap="ROUND";parent.appendChild(h);
1704
+ var v=figma.createVector();v.vectorPaths=[{windingRule:"NONZERO",data:vPath}];
1705
+ v.strokes=[{type:"SOLID",color:color}];v.strokeWeight=1.5;v.fills=[];v.strokeCap="ROUND";parent.appendChild(v);
1706
+ }
1707
+ var bArm=Math.min(12,cW/6,cH/6);
1708
+ if(pt>0){
1709
+ drawBracket(canvas,compX,compY+pt,"TL",bArm,padColor);
1710
+ drawBracket(canvas,compX+cW,compY+pt,"TR",bArm,padColor);
1711
+ }
1712
+ if(pb>0){
1713
+ drawBracket(canvas,compX,compY+cH-pb,"BL",bArm,padColor);
1714
+ drawBracket(canvas,compX+cW,compY+cH-pb,"BR",bArm,padColor);
1715
+ }
1716
+ if(pl>0){
1717
+ drawBracket(canvas,compX+pl,compY,"TL",bArm,padColor);
1718
+ drawBracket(canvas,compX+pl,compY+cH,"BL",bArm,padColor);
1719
+ }
1720
+ if(pr>0){
1721
+ drawBracket(canvas,compX+cW-pr,compY,"TR",bArm,padColor);
1722
+ drawBracket(canvas,compX+cW-pr,compY+cH,"BR",bArm,padColor);
1723
+ }
1724
+
1725
+ // Green padding pills with px suffix
1726
+ function padPill(x,y,value){
1727
+ var pill=figma.createFrame();pill.name="PadPill";
1728
+ pill.layoutMode="HORIZONTAL";
1729
+ pill.primaryAxisSizingMode="AUTO";pill.counterAxisSizingMode="AUTO";
1730
+ pill.paddingTop=2;pill.paddingBottom=2;pill.paddingLeft=4;pill.paddingRight=4;
1731
+ pill.fills=[{type:"SOLID",color:padColor}];pill.cornerRadius=4;
1732
+ canvas.appendChild(pill);pill.x=x;pill.y=y;
1733
+ var pTxt=figma.createText();pTxt.fontName=F.r;pTxt.fontSize=11;
1734
+ pTxt.characters=value+"px";
1735
+ pTxt.fills=[{type:"SOLID",color:{r:1,g:1,b:1}}];
1736
+ pTxt.textAutoResize="WIDTH_AND_HEIGHT";
1737
+ pill.appendChild(pTxt);
1738
+ }
1739
+ if(pt>0)padPill(compX+cW/2-12,compY+2,pt);
1740
+ if(pb>0)padPill(compX+cW/2-12,compY+cH-16,pb);
1741
+ if(pl>0)padPill(compX-28,compY+cH/2-8,pl);
1742
+ if(pr>0)padPill(compX+cW+4,compY+cH/2-8,pr);
1743
+
1744
+ // ── Orange gap dashed lines + pills between children ──
1745
+ var children=${childrenData};
1746
+ var layoutMode="${root.layoutMode}";
1747
+ var itemSpacing=${root.itemSpacing};
1748
+ if(children.length>1&&itemSpacing>0){
1749
+ for(var ci=0;ci<children.length-1;ci++){
1750
+ var c1=children[ci],c2=children[ci+1];
1751
+ if(layoutMode==="HORIZONTAL"){
1752
+ var gapX1=compX+c1.x+c1.w;
1753
+ var gapX2=compX+c2.x;
1754
+ var gapW=gapX2-gapX1;
1755
+ if(gapW>0){
1756
+ // Dashed line spanning the gap
1757
+ var gLine=figma.createVector();
1758
+ gLine.vectorPaths=[{windingRule:"NONZERO",data:"M "+gapX1+" "+(compY+cH/2)+" L "+gapX2+" "+(compY+cH/2)}];
1759
+ gLine.strokes=[{type:"SOLID",color:gapColor}];gLine.strokeWeight=1;gLine.fills=[];
1760
+ gLine.dashPattern=[4,4];canvas.appendChild(gLine);
1761
+ // Gap pill centered above
1762
+ var gPill=figma.createFrame();gPill.name="GapPill";
1763
+ gPill.layoutMode="HORIZONTAL";
1764
+ gPill.primaryAxisSizingMode="AUTO";gPill.counterAxisSizingMode="AUTO";
1765
+ gPill.paddingTop=2;gPill.paddingBottom=2;gPill.paddingLeft=4;gPill.paddingRight=4;
1766
+ gPill.fills=[{type:"SOLID",color:gapColor}];gPill.cornerRadius=4;
1767
+ canvas.appendChild(gPill);gPill.x=gapX1+gapW/2-12;gPill.y=compY+cH/2-18;
1768
+ var gTxt=figma.createText();gTxt.fontName=F.r;gTxt.fontSize=11;
1769
+ gTxt.characters=String(itemSpacing)+"px";
1770
+ gTxt.fills=[{type:"SOLID",color:{r:1,g:1,b:1}}];
1771
+ gTxt.textAutoResize="WIDTH_AND_HEIGHT";
1772
+ gPill.appendChild(gTxt);
1773
+ }
1774
+ }else{
1775
+ var gapY1=compY+c1.y+c1.h;
1776
+ var gapY2=compY+c2.y;
1777
+ var gapH=gapY2-gapY1;
1778
+ if(gapH>0){
1779
+ var gLine=figma.createVector();
1780
+ gLine.vectorPaths=[{windingRule:"NONZERO",data:"M "+(compX+cW/2)+" "+gapY1+" L "+(compX+cW/2)+" "+gapY2}];
1781
+ gLine.strokes=[{type:"SOLID",color:gapColor}];gLine.strokeWeight=1;gLine.fills=[];
1782
+ gLine.dashPattern=[4,4];canvas.appendChild(gLine);
1783
+ var gPill=figma.createFrame();gPill.name="GapPill";
1784
+ gPill.layoutMode="HORIZONTAL";
1785
+ gPill.primaryAxisSizingMode="AUTO";gPill.counterAxisSizingMode="AUTO";
1786
+ gPill.paddingTop=2;gPill.paddingBottom=2;gPill.paddingLeft=4;gPill.paddingRight=4;
1787
+ gPill.fills=[{type:"SOLID",color:gapColor}];gPill.cornerRadius=4;
1788
+ canvas.appendChild(gPill);gPill.x=compX+cW+8;gPill.y=gapY1+gapH/2-8;
1789
+ var gTxt=figma.createText();gTxt.fontName=F.r;gTxt.fontSize=11;
1790
+ gTxt.characters=String(itemSpacing)+"px";
1791
+ gTxt.fills=[{type:"SOLID",color:{r:1,g:1,b:1}}];
1792
+ gTxt.textAutoResize="WIDTH_AND_HEIGHT";
1793
+ gPill.appendChild(gTxt);
1794
+ }
1795
+ }
1796
+ }
1797
+ }
1798
+
1799
+ // ── Blue child element overlays (subtle) ──
1800
+ for(var chi=0;chi<children.length;chi++){
1801
+ var ch=children[chi];
1802
+ var chRect=figma.createRectangle();
1803
+ chRect.resize(ch.w,ch.h);chRect.x=compX+ch.x;chRect.y=compY+ch.y;
1804
+ chRect.fills=[{type:"SOLID",color:sizeColor}];chRect.opacity=0.08;
1805
+ canvas.appendChild(chRect);
1806
+ }
1807
+
1808
+ // ── Blue dimension lines (width above, height left) ──
1809
+ function drawDimLine(parent,x1,y1,x2,y2,label,color){
1810
+ var isH=Math.abs(y2-y1)<Math.abs(x2-x1);
1811
+ var line=figma.createVector();
1812
+ line.vectorPaths=[{windingRule:"NONZERO",data:"M "+x1+" "+y1+" L "+x2+" "+y2}];
1813
+ line.strokes=[{type:"SOLID",color:color}];
1814
+ line.strokeWeight=${LINE_W};line.fills=[];line.strokeCap="ROUND";
1815
+ parent.appendChild(line);
1816
+ var capLen=4;
1817
+ if(isH){
1818
+ var c1=figma.createVector();
1819
+ c1.vectorPaths=[{windingRule:"NONZERO",data:"M "+x1+" "+(y1-capLen)+" L "+x1+" "+(y1+capLen)}];
1820
+ c1.strokes=[{type:"SOLID",color:color}];c1.strokeWeight=${LINE_W};c1.fills=[];parent.appendChild(c1);
1821
+ var c2=figma.createVector();
1822
+ c2.vectorPaths=[{windingRule:"NONZERO",data:"M "+x2+" "+(y2-capLen)+" L "+x2+" "+(y2+capLen)}];
1823
+ c2.strokes=[{type:"SOLID",color:color}];c2.strokeWeight=${LINE_W};c2.fills=[];parent.appendChild(c2);
1824
+ }else{
1825
+ var c1=figma.createVector();
1826
+ c1.vectorPaths=[{windingRule:"NONZERO",data:"M "+(x1-capLen)+" "+y1+" L "+(x1+capLen)+" "+y1}];
1827
+ c1.strokes=[{type:"SOLID",color:color}];c1.strokeWeight=${LINE_W};c1.fills=[];parent.appendChild(c1);
1828
+ var c2=figma.createVector();
1829
+ c2.vectorPaths=[{windingRule:"NONZERO",data:"M "+(x2-capLen)+" "+y2+" L "+(x2+capLen)+" "+y2}];
1830
+ c2.strokes=[{type:"SOLID",color:color}];c2.strokeWeight=${LINE_W};c2.fills=[];parent.appendChild(c2);
1831
+ }
1832
+ var lbl=figma.createText();lbl.fontName=F.mono;lbl.fontSize=10;
1833
+ lbl.characters=label;lbl.fills=[{type:"SOLID",color:color}];
1834
+ lbl.textAutoResize="WIDTH_AND_HEIGHT";parent.appendChild(lbl);
1835
+ var mx=(x1+x2)/2,my=(y1+y2)/2;
1836
+ if(isH){lbl.x=mx-lbl.width/2;lbl.y=my-lbl.height-4;}
1837
+ else{lbl.x=mx+4;lbl.y=my-lbl.height/2;}
1838
+ }
1839
+
1840
+ // Width dimension (above component)
1841
+ drawDimLine(canvas,compX,compY-20,compX+cW,compY-20,Math.round(cW)+"px",sizeColor);
1842
+ // Height dimension (left of component)
1843
+ drawDimLine(canvas,compX-20,compY,compX-20,compY+cH,Math.round(cH)+"px",sizeColor);
1844
+
1845
+ // Resize canvas to fit
1846
+ canvas.resize(Math.max(cW+AM*2,500),cH+AM*2);
1847
+
1848
+ // ── Right: Content panel ──
1849
+ var spcCnt=figma.createFrame();spcCnt.name="Spacing Details";
1850
+ spcCnt.layoutMode="VERTICAL";
1851
+ spcCnt.primaryAxisSizingMode="AUTO";spcCnt.counterAxisSizingMode="AUTO";
1852
+ spcCnt.itemSpacing=12;spcCnt.fills=[];
1853
+ spcExh.appendChild(spcCnt);spcCnt.layoutSizingHorizontal="FILL";
1854
+
1855
+ // Element header with type icon
1856
+ var spcElRow=figma.createFrame();spcElRow.name="ElementHeader";
1857
+ spcElRow.layoutMode="HORIZONTAL";
1858
+ spcElRow.primaryAxisSizingMode="AUTO";spcElRow.counterAxisSizingMode="AUTO";
1859
+ spcElRow.itemSpacing=6;spcElRow.fills=[];spcElRow.counterAxisAlignItems="CENTER";
1860
+ spcCnt.appendChild(spcElRow);spcElRow.layoutSizingHorizontal="FILL";
1861
+
1862
+ // Type icon (diamond for component)
1863
+ var spcIcon=figma.createFrame();spcIcon.name="TypeIcon";
1864
+ spcIcon.layoutMode="HORIZONTAL";
1865
+ spcIcon.primaryAxisSizingMode="FIXED";spcIcon.counterAxisSizingMode="FIXED";
1866
+ spcIcon.resize(20,20);spcIcon.fills=[];
1867
+ spcIcon.strokes=[{type:"SOLID",color:${C.textMuted}}];
1868
+ spcIcon.strokeWeight=1;spcIcon.strokeAlign="INSIDE";spcIcon.cornerRadius=3;
1869
+ spcIcon.primaryAxisAlignItems="CENTER";spcIcon.counterAxisAlignItems="CENTER";
1870
+ spcElRow.appendChild(spcIcon);
1871
+ var spcIconLbl=figma.createText();spcIconLbl.fontName=F.r;spcIconLbl.fontSize=10;
1872
+ spcIconLbl.characters="\\u25C7";
1873
+ spcIconLbl.fills=[{type:"SOLID",color:${C.textMuted}}];
1874
+ spcIconLbl.textAutoResize="WIDTH_AND_HEIGHT";
1875
+ spcIcon.appendChild(spcIconLbl);
1876
+
1877
+ // Element name
1878
+ var spcName=figma.createText();spcName.fontName=F.b;spcName.fontSize=20;
1879
+ spcName.lineHeight={unit:"PIXELS",value:28};
1880
+ spcName.characters=${esc(root.element)};
1881
+ spcName.fills=[{type:"SOLID",color:${C.textMain}}];
1882
+ spcName.textAutoResize="WIDTH_AND_HEIGHT";
1883
+ spcElRow.appendChild(spcName);
1884
+
1885
+ // Attributes — two-column layout (muted label + semibold value)
1886
+ function spcAttr(parent,label,value){
1887
+ var row=figma.createFrame();row.name=label;
1888
+ row.layoutMode="HORIZONTAL";
1889
+ row.primaryAxisSizingMode="AUTO";row.counterAxisSizingMode="AUTO";
1890
+ row.itemSpacing=8;row.fills=[];
1891
+ parent.appendChild(row);row.layoutSizingHorizontal="FILL";
1892
+ var lTxt=figma.createText();lTxt.fontName=F.r;lTxt.fontSize=12;
1893
+ lTxt.lineHeight={unit:"PIXELS",value:18};lTxt.characters=label;
1894
+ lTxt.fills=[{type:"SOLID",color:${C.textMuted}}];lTxt.textAutoResize="HEIGHT";
1895
+ lTxt.resize(140,18);row.appendChild(lTxt);lTxt.layoutSizingHorizontal="FIXED";
1896
+ var vTxt=figma.createText();vTxt.fontName=F.sb;vTxt.fontSize=12;
1897
+ vTxt.lineHeight={unit:"PIXELS",value:18};vTxt.characters=String(value);
1898
+ vTxt.fills=[{type:"SOLID",color:${C.textMain}}];vTxt.textAutoResize="HEIGHT";
1899
+ row.appendChild(vTxt);vTxt.layoutSizingHorizontal="FILL";
1900
+ }
1901
+
1902
+ spcAttr(spcCnt,"Direction",${esc(root.layoutMode === "HORIZONTAL" ? "Horizontal" : root.layoutMode === "VERTICAL" ? "Vertical" : "None")});
1903
+ spcAttr(spcCnt,"Vertical resizing",${esc(root.layoutSizingV || "Fixed")});
1904
+ spcAttr(spcCnt,"Horizontal resizing",${esc(root.layoutSizingH || "Fixed")});
1905
+ spcAttr(spcCnt,"Item spacing",String(${root.itemSpacing}));
1906
+ spcAttr(spcCnt,"Padding top",String(${root.paddingTop}));
1907
+ spcAttr(spcCnt,"Padding bottom",String(${root.paddingBottom}));
1908
+ spcAttr(spcCnt,"Padding left",String(${root.paddingLeft}));
1909
+ spcAttr(spcCnt,"Padding right",String(${root.paddingRight}));
1910
+
1911
+ // ── Legend ──
1912
+ var spcLeg=figma.createFrame();spcLeg.name="Legend";
1913
+ spcLeg.layoutMode="HORIZONTAL";
1914
+ spcLeg.primaryAxisSizingMode="AUTO";spcLeg.counterAxisSizingMode="AUTO";
1915
+ spcLeg.itemSpacing=24;spcLeg.fills=[];spcLeg.paddingTop=16;
1916
+ spcCard.appendChild(spcLeg);spcLeg.layoutSizingHorizontal="FILL";
1917
+
1918
+ function legItem(parent,color,label){
1919
+ var li=figma.createFrame();li.layoutMode="HORIZONTAL";
1920
+ li.primaryAxisSizingMode="AUTO";li.counterAxisSizingMode="AUTO";
1921
+ li.itemSpacing=8;li.fills=[];li.counterAxisAlignItems="CENTER";
1922
+ parent.appendChild(li);
1923
+ var sw=figma.createRectangle();sw.resize(12,12);
1924
+ sw.fills=[{type:"SOLID",color:color}];sw.cornerRadius=2;li.appendChild(sw);
1925
+ var lt=figma.createText();lt.fontName=F.r;lt.fontSize=12;
1926
+ lt.characters=label;lt.fills=[{type:"SOLID",color:${C.textMuted}}];
1927
+ lt.textAutoResize="WIDTH_AND_HEIGHT";li.appendChild(lt);
1928
+ }
1929
+ legItem(spcLeg,padColor,"Padding");
1930
+ legItem(spcLeg,gapColor,"Item spacing");
1931
+ legItem(spcLeg,sizeColor,"Element sizing");
1932
+
1933
+ return{ok:true};
1934
+ })();
1935
+ `);
1936
+ if (!r.success) console.error("Spacing exhibit failed:", r.error);
1937
+ }
1938
+
1939
+ // ── Color Token Exhibit ─────────────────────────────────────────────────
1940
+
1941
+ async function renderColorExhibit(
1942
+ bridge: Bridge, mId: string, spec: ComponentSpec, sectionNum?: number,
1943
+ ): Promise<void> {
1944
+ const tokens = spec.extraction.colorTokens;
1945
+ if (tokens.length === 0) return;
1946
+
1947
+ const seen = new Set<string>();
1948
+ const unique = tokens.filter(t => {
1949
+ const key = `${t.colorHex}|${t.tokenName}`;
1950
+ if (seen.has(key)) return false;
1951
+ seen.add(key);
1952
+ return true;
1953
+ }).slice(0, 30);
1954
+
1955
+ // Group tokens by semantic role derived from token name or element
1956
+ const rolePatterns: [RegExp, string][] = [
1957
+ [/primary|brand/i, "Primary"],
1958
+ [/secondary/i, "Secondary"],
1959
+ [/danger|error|destructive/i, "Danger"],
1960
+ [/warning|caution/i, "Warning"],
1961
+ [/success|positive/i, "Success"],
1962
+ [/interactive|action|link/i, "Interactive"],
1963
+ [/disabled|inactive/i, "Disabled"],
1964
+ [/inverse|on-color/i, "Inverse"],
1965
+ [/border|stroke|outline/i, "Border"],
1966
+ [/background|bg|surface|layer/i, "Background"],
1967
+ [/text|label|content|foreground/i, "Text"],
1968
+ [/icon/i, "Icon"],
1969
+ [/focus|ring/i, "Focus"],
1970
+ [/hover/i, "Hover"],
1971
+ ];
1972
+
1973
+ function classifyToken(t: { tokenName: string; element: string }): string {
1974
+ const combined = `${t.tokenName} ${t.element}`;
1975
+ for (const [re, label] of rolePatterns) {
1976
+ if (re.test(combined)) return label;
1977
+ }
1978
+ return "Other";
1979
+ }
1980
+
1981
+ // Build grouped structure
1982
+ const groupMap = new Map<string, typeof unique>();
1983
+ for (const t of unique) {
1984
+ const role = classifyToken(t);
1985
+ if (!groupMap.has(role)) groupMap.set(role, []);
1986
+ groupMap.get(role)!.push(t);
1987
+ }
1988
+ const groups = Array.from(groupMap.entries());
1989
+
1990
+ const groupsJSON = JSON.stringify(groups);
1991
+
1992
+ const r = await bridge.execute(`
1993
+ (async () => {
1994
+ ${FL}
1995
+ var m=await figma.getNodeByIdAsync(${esc(mId)});
1996
+ if(!m)return{error:"no master"};
1997
+
1998
+ ${sectionCardOpenJS("colCard", "m", "Color Tokens")}
1999
+ ${sectionTitleJS("colTitle", "colCard", "Color Tokens by Type", sectionNum)}
2000
+
2001
+ var groups=${groupsJSON};
2002
+
2003
+ for(var gi=0;gi<groups.length;gi++){
2004
+ var groupName=groups[gi][0];
2005
+ var groupTokens=groups[gi][1];
2006
+
2007
+ // Group sub-header
2008
+ var grpTitle=figma.createText();grpTitle.fontName=F.b;grpTitle.fontSize=16;
2009
+ grpTitle.lineHeight={unit:"PIXELS",value:24};grpTitle.characters=groupName;
2010
+ grpTitle.fills=[{type:"SOLID",color:${C.textMain}}];
2011
+ grpTitle.textAutoResize="HEIGHT";
2012
+ colCard.appendChild(grpTitle);grpTitle.layoutSizingHorizontal="FILL";
2013
+
2014
+ // Horizontal swatch row for this group
2015
+ var swatchRow=figma.createFrame();swatchRow.name=groupName+" Swatches";
2016
+ swatchRow.layoutMode="HORIZONTAL";swatchRow.layoutWrap="WRAP";
2017
+ swatchRow.primaryAxisSizingMode="AUTO";swatchRow.counterAxisSizingMode="AUTO";
2018
+ swatchRow.itemSpacing=12;swatchRow.counterAxisSpacing=12;swatchRow.fills=[];
2019
+ colCard.appendChild(swatchRow);swatchRow.layoutSizingHorizontal="FILL";
2020
+
2021
+ for(var ti=0;ti<groupTokens.length;ti++){
2022
+ var t=groupTokens[ti];
2023
+
2024
+ // Swatch card: color block + label
2025
+ var sc=figma.createFrame();sc.name=t.tokenName||t.colorHex;
2026
+ sc.layoutMode="VERTICAL";
2027
+ sc.primaryAxisSizingMode="AUTO";sc.counterAxisSizingMode="FIXED";
2028
+ sc.resize(140,10);
2029
+ sc.itemSpacing=6;sc.paddingTop=8;sc.paddingBottom=10;sc.paddingLeft=8;sc.paddingRight=8;
2030
+ sc.fills=[{type:"SOLID",color:${C.bgCard}}];sc.cornerRadius=8;
2031
+ swatchRow.appendChild(sc);
2032
+
2033
+ // Color swatch rectangle
2034
+ var sw=figma.createRectangle();sw.resize(124,36);sw.cornerRadius=6;
2035
+ var hex=t.colorHex.replace("#","");
2036
+ var cr=parseInt(hex.substring(0,2),16)/255;
2037
+ var cg=parseInt(hex.substring(2,4),16)/255;
2038
+ var cb=parseInt(hex.substring(4,6),16)/255;
2039
+ sw.fills=[{type:"SOLID",color:{r:cr,g:cg,b:cb}}];
2040
+ sw.strokes=[{type:"SOLID",color:${C.border}}];sw.strokeWeight=1;sw.strokeAlign="INSIDE";
2041
+ sc.appendChild(sw);sw.layoutSizingHorizontal="FILL";
2042
+
2043
+ // Hex label
2044
+ var hexLbl=figma.createText();hexLbl.fontName=F.mono;hexLbl.fontSize=11;
2045
+ hexLbl.lineHeight={unit:"PIXELS",value:16};hexLbl.characters=t.colorHex.toUpperCase();
2046
+ hexLbl.fills=[{type:"SOLID",color:${C.textMain}}];
2047
+ hexLbl.textAutoResize="HEIGHT";
2048
+ sc.appendChild(hexLbl);hexLbl.layoutSizingHorizontal="FILL";
2049
+
2050
+ // Token name
2051
+ if(t.tokenName){
2052
+ var tokLbl=figma.createText();tokLbl.fontName=F.r;tokLbl.fontSize=10;
2053
+ tokLbl.lineHeight={unit:"PIXELS",value:14};tokLbl.characters=t.tokenName;
2054
+ tokLbl.fills=[{type:"SOLID",color:${C.textMuted}}];
2055
+ tokLbl.textAutoResize="HEIGHT";
2056
+ sc.appendChild(tokLbl);tokLbl.layoutSizingHorizontal="FILL";
2057
+ }
2058
+ }
2059
+
2060
+ // Spacer between groups (except last)
2061
+ if(gi<groups.length-1){
2062
+ var sep=figma.createFrame();sep.resize(4,8);sep.fills=[];
2063
+ colCard.appendChild(sep);sep.layoutSizingHorizontal="FILL";
2064
+ }
2065
+ }
2066
+
2067
+ return{ok:true};
2068
+ })();
2069
+ `);
2070
+ if (!r.success) console.error("Color exhibit failed:", r.error);
2071
+ }
2072
+
2073
+ // ── Typography Exhibit ──────────────────────────────────────────────────
2074
+
2075
+ async function renderTypographyExhibit(
2076
+ bridge: Bridge, mId: string, spec: ComponentSpec, sectionNum?: number,
2077
+ ): Promise<void> {
2078
+ const typo = spec.extraction.typography;
2079
+ if (typo.length === 0) return;
2080
+
2081
+ const seen = new Set<string>();
2082
+ const unique = typo.filter(t => {
2083
+ const key = `${t.fontFamily}|${t.fontStyle}|${t.fontSize}`;
2084
+ if (seen.has(key)) return false;
2085
+ seen.add(key);
2086
+ return true;
2087
+ }).slice(0, 16);
2088
+
2089
+ const typoJSON = JSON.stringify(unique.map(t => ({
2090
+ element: t.element,
2091
+ sample: (t.characters || "").slice(0, 40) || t.element,
2092
+ family: t.fontFamily,
2093
+ style: t.fontStyle,
2094
+ size: t.fontSize,
2095
+ lineHeight: t.lineHeightPx,
2096
+ letterSpacing: t.letterSpacing,
2097
+ token: t.tokenName,
2098
+ })));
2099
+
2100
+ const r = await bridge.execute(`
2101
+ (async () => {
2102
+ ${FL}
2103
+ var m=await figma.getNodeByIdAsync(${esc(mId)});
2104
+ if(!m)return{error:"no master"};
2105
+
2106
+ ${sectionCardOpenJS("typCard", "m", "Typography")}
2107
+ ${sectionTitleJS("typTitle", "typCard", "Typography", sectionNum)}
2108
+
2109
+ var typo=${typoJSON};
2110
+
2111
+ // Batch-load all unique fonts
2112
+ var fontCache={};
2113
+ for(var fi=0;fi<typo.length;fi++){
2114
+ var fk=typo[fi].family+"|"+typo[fi].style;
2115
+ if(!fontCache[fk]){
2116
+ try{
2117
+ await figma.loadFontAsync({family:typo[fi].family,style:typo[fi].style});
2118
+ fontCache[fk]={family:typo[fi].family,style:typo[fi].style};
2119
+ }catch(e){
2120
+ try{
2121
+ await figma.loadFontAsync({family:typo[fi].family,style:"Regular"});
2122
+ fontCache[fk]={family:typo[fi].family,style:"Regular"};
2123
+ }catch(e2){fontCache[fk]=F.r;}
2124
+ }
2125
+ }
2126
+ }
2127
+
2128
+ var outer=figma.createFrame();outer.name="Typography Table";
2129
+ outer.layoutMode="VERTICAL";
2130
+ outer.primaryAxisSizingMode="AUTO";outer.counterAxisSizingMode="AUTO";
2131
+ outer.itemSpacing=0;outer.fills=[];
2132
+ outer.cornerRadius=8;
2133
+ outer.strokes=[{type:"SOLID",color:${C.border}}];outer.strokeWeight=1;outer.strokeAlign="INSIDE";
2134
+ outer.clipsContent=true;
2135
+ typCard.appendChild(outer);outer.layoutSizingHorizontal="FILL";
2136
+
2137
+ for(var i=0;i<typo.length;i++){
2138
+ var t=typo[i];
2139
+ var actualSize=Math.min(t.size||14,72);
2140
+ var rowPadV=Math.max(16,Math.round(actualSize*0.4));
2141
+
2142
+ var row=figma.createFrame();row.name=t.element;
2143
+ row.layoutMode="HORIZONTAL";
2144
+ row.primaryAxisSizingMode="AUTO";row.counterAxisSizingMode="AUTO";
2145
+ row.itemSpacing=24;
2146
+ row.paddingTop=rowPadV;row.paddingBottom=rowPadV;row.paddingLeft=20;row.paddingRight=20;
2147
+ row.fills=[{type:"SOLID",color:i%2===0?${C.white}:${C.bgCard}}];
2148
+ row.counterAxisAlignItems="CENTER";
2149
+ if(i<typo.length-1){
2150
+ row.strokes=[{type:"SOLID",color:${C.border}}];row.strokeWeight=1;
2151
+ row.strokeTopWeight=0;row.strokeRightWeight=0;row.strokeBottomWeight=1;row.strokeLeftWeight=0;
2152
+ row.strokeAlign="INSIDE";
2153
+ }
2154
+ outer.appendChild(row);row.layoutSizingHorizontal="FILL";
2155
+
2156
+ var eName=figma.createText();eName.fontName=F.sb;eName.fontSize=13;
2157
+ eName.lineHeight={unit:"PIXELS",value:20};eName.characters=t.element;
2158
+ eName.fills=[{type:"SOLID",color:${C.textMuted}}];
2159
+ eName.textAutoResize="HEIGHT";eName.resize(140,20);
2160
+ row.appendChild(eName);eName.layoutSizingHorizontal="FIXED";
2161
+
2162
+ // Render sample at actual font size and family
2163
+ var sampleFont=fontCache[t.family+"|"+t.style]||F.r;
2164
+ var sample=figma.createText();sample.fontName=sampleFont;sample.fontSize=actualSize;
2165
+ sample.lineHeight={unit:"PIXELS",value:Math.round(actualSize*1.4)};
2166
+ sample.characters=t.sample;
2167
+ sample.fills=[{type:"SOLID",color:${C.textMain}}];
2168
+ sample.textAutoResize="HEIGHT";
2169
+ row.appendChild(sample);sample.layoutSizingHorizontal="FILL";
2170
+
2171
+ var spec=t.family+" "+t.style+" "+t.size;
2172
+ if(t.lineHeight)spec+="/"+Math.round(t.lineHeight);
2173
+ var specTxt=figma.createText();specTxt.fontName=F.mono;specTxt.fontSize=11;
2174
+ specTxt.lineHeight={unit:"PIXELS",value:16};specTxt.characters=spec;
2175
+ specTxt.fills=[{type:"SOLID",color:${C.textMuted}}];
2176
+ specTxt.textAutoResize="HEIGHT";specTxt.resize(220,16);
2177
+ row.appendChild(specTxt);specTxt.layoutSizingHorizontal="FIXED";
2178
+
2179
+ if(t.token){
2180
+ var tokTxt=figma.createText();tokTxt.fontName=F.mono;tokTxt.fontSize=11;
2181
+ tokTxt.lineHeight={unit:"PIXELS",value:16};tokTxt.characters=t.token;
2182
+ tokTxt.fills=[{type:"SOLID",color:${C.padGreen}}];
2183
+ tokTxt.textAutoResize="WIDTH_AND_HEIGHT";
2184
+ row.appendChild(tokTxt);
2185
+ }
2186
+ }
2187
+
2188
+ return{ok:true};
2189
+ })();
2190
+ `);
2191
+ if (!r.success) console.error("Typography exhibit failed:", r.error);
2192
+ }
2193
+
2194
+ // ══════════════════════════════════════════════════════════════════════════
2195
+ // MAIN ENTRY — routes visual sections to exhibits, others to text cards
2196
+ // ══════════════════════════════════════════════════════════════════════════
2197
+
2198
+ export async function renderVisualDoc(spec: ComponentSpec, pageName?: string): Promise<string> {
2199
+ const bridge = await getBridge();
2200
+ const resolvedName = pageName || `${spec.componentName} \u2014 Spec`;
2201
+
2202
+ // 1. Page + master frame
2203
+ const { pageId, mId } = await initPage(bridge, resolvedName);
2204
+
2205
+ // 2. Header + Hero (merged into one call)
2206
+ const description = spec.description
2207
+ || `${spec.componentName} is a ${(spec.nodeType || "component").toLowerCase().replace(/_/g, " ")} used in the design system.`;
2208
+ // Derive section labels for subtitle
2209
+ const sectionLabelMap: Record<string, string> = {
2210
+ anatomy: "Anatomy", variants: "Properties", spacing: "Spacing",
2211
+ "color-tokens": "Color Tokens", typography: "Typography",
2212
+ };
2213
+ const sectionLabels = spec.sections
2214
+ .map(s => sectionLabelMap[s.id])
2215
+ .filter((l): l is string => !!l);
2216
+ await renderDocHeaderAndHero(bridge, mId, spec.componentName, description, spec, sectionLabels);
2217
+
2218
+ // 3. Sections — route visual sections to exhibit renderers, others to text cards
2219
+ let sectionNumber = 0;
2220
+ for (let i = 0; i < spec.sections.length; i++) {
2221
+ const sec = spec.sections[i];
2222
+ sectionNumber++;
2223
+
2224
+ switch (sec.id) {
2225
+ // ── Visual exhibits (each creates its own card) ──
2226
+ case "anatomy":
2227
+ if (spec.extraction.anatomy.elements.length > 0) {
2228
+ await renderAnatomyExhibit(bridge, mId, spec, sectionNumber);
2229
+ } else {
2230
+ await renderTextSection(bridge, mId, sec.title, sec.content, sectionNumber);
2231
+ }
2232
+ break;
2233
+
2234
+ case "variants":
2235
+ if (spec.extraction.snapshot.variants?.length > 1) {
2236
+ await renderPropertyTable(bridge, mId, spec, sectionNumber);
2237
+ } else {
2238
+ await renderTextSection(bridge, mId, sec.title, sec.content, sectionNumber);
2239
+ }
2240
+ break;
2241
+
2242
+ case "spacing":
2243
+ // Render side-by-side sizing comparison if a Size axis exists
2244
+ await renderSizingComparison(bridge, mId, spec, sectionNumber);
2245
+ if (spec.extraction.spacing.length > 0) {
2246
+ sectionNumber++;
2247
+ await renderSpacingExhibit(bridge, mId, spec, sectionNumber);
2248
+ } else if (!(spec.extraction.snapshot.variantGroupProperties || {})[
2249
+ Object.keys(spec.extraction.snapshot.variantGroupProperties || {}).find(k => /^size$/i.test(k)) || ""
2250
+ ]) {
2251
+ await renderTextSection(bridge, mId, sec.title, sec.content, sectionNumber);
2252
+ }
2253
+ break;
2254
+
2255
+ case "color-tokens":
2256
+ if (spec.extraction.colorTokens.length > 0) {
2257
+ await renderColorExhibit(bridge, mId, spec, sectionNumber);
2258
+ } else {
2259
+ await renderTextSection(bridge, mId, sec.title, sec.content, sectionNumber);
2260
+ }
2261
+ break;
2262
+
2263
+ case "typography":
2264
+ if (spec.extraction.typography.length > 0) {
2265
+ await renderTypographyExhibit(bridge, mId, spec, sectionNumber);
2266
+ } else {
2267
+ await renderTextSection(bridge, mId, sec.title, sec.content, sectionNumber);
2268
+ }
2269
+ break;
2270
+
2271
+ // ── Text-based sections (each in its own white card) ──
2272
+ default:
2273
+ await renderTextSection(bridge, mId, sec.title, sec.content, sectionNumber);
2274
+ }
2275
+ }
2276
+
2277
+ // 3b. Footer + Zoom to fit (merged into one call)
2278
+ {
2279
+ const timestamp = new Date().toLocaleDateString("en-US", {
2280
+ year: "numeric", month: "long", day: "numeric",
2281
+ });
2282
+ try {
2283
+ await bridge.execute(`
2284
+ (async () => {
2285
+ ${FL}
2286
+ var m=await figma.getNodeByIdAsync(${esc(mId)});
2287
+ if(!m)return{error:"no master"};
2288
+
2289
+ // Footer
2290
+ var footer=figma.createFrame();footer.name="Footer";
2291
+ footer.layoutMode="HORIZONTAL";
2292
+ footer.primaryAxisSizingMode="AUTO";footer.counterAxisSizingMode="AUTO";
2293
+ footer.itemSpacing=0;footer.fills=[];
2294
+ footer.paddingTop=16;footer.paddingBottom=0;
2295
+ footer.paddingLeft=8;footer.paddingRight=8;
2296
+ footer.strokes=[{type:"SOLID",color:${C.border}}];
2297
+ footer.strokeWeight=1;footer.strokeAlign="INSIDE";
2298
+ footer.strokeTopWeight=1;footer.strokeRightWeight=0;
2299
+ footer.strokeBottomWeight=0;footer.strokeLeftWeight=0;
2300
+ footer.counterAxisAlignItems="CENTER";
2301
+ m.appendChild(footer);footer.layoutSizingHorizontal="FILL";
2302
+
2303
+ var left=figma.createText();left.fontName=F.r;left.fontSize=12;
2304
+ left.lineHeight={unit:"PIXELS",value:18};
2305
+ left.characters="Generated by MCP Power";
2306
+ left.fills=[{type:"SOLID",color:${C.textMuted}}];
2307
+ left.textAutoResize="WIDTH_AND_HEIGHT";
2308
+ footer.appendChild(left);
2309
+
2310
+ var spacer=figma.createFrame();spacer.name="Spacer";
2311
+ spacer.resize(4,4);spacer.fills=[];
2312
+ footer.appendChild(spacer);spacer.layoutGrow=1;
2313
+
2314
+ var right=figma.createText();right.fontName=F.r;right.fontSize=12;
2315
+ right.lineHeight={unit:"PIXELS",value:18};
2316
+ right.characters=${esc(timestamp)};
2317
+ right.fills=[{type:"SOLID",color:${C.textMuted}}];
2318
+ right.textAutoResize="WIDTH_AND_HEIGHT";
2319
+ footer.appendChild(right);
2320
+
2321
+ // Zoom to fit
2322
+ var pg=await figma.getNodeByIdAsync(${esc(pageId)});
2323
+ if(pg&&pg.type==="PAGE"&&pg.children.length>0)
2324
+ figma.viewport.scrollAndZoomIntoView(pg.children);
2325
+
2326
+ return{ok:true};
2327
+ })();
2328
+ `);
2329
+ } catch { /* ok */ }
2330
+ }
2331
+
2332
+ return pageId;
2333
+ }