create-tanqory-theme 0.0.0-stage → 0.2.3

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 (879) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -2
  3. package/bin/create-tanqory-theme.mjs +13 -0
  4. package/lib/agents.mjs +118 -0
  5. package/lib/create.mjs +245 -0
  6. package/package.json +39 -4
  7. package/templates/blank/README.md +13 -0
  8. package/templates/blank/config/settings.json +3 -0
  9. package/templates/blank/locales/en.json +22 -0
  10. package/templates/blank/locales/th.json +22 -0
  11. package/templates/blank/package.json +34 -0
  12. package/templates/blank/regions/footer.json +35 -0
  13. package/templates/blank/regions/header.json +13 -0
  14. package/templates/blank/scripts/build.mjs +193 -0
  15. package/templates/blank/scripts/probe.ts +4 -0
  16. package/templates/blank/src/bundle.ts +15 -0
  17. package/templates/blank/src/components/parts.tsx +32 -0
  18. package/templates/blank/src/entry.client.tsx +6 -0
  19. package/templates/blank/src/entry.server.tsx +13 -0
  20. package/templates/blank/src/sections/chrome.tsx +132 -0
  21. package/templates/blank/src/sections/commerce.tsx +242 -0
  22. package/templates/blank/src/sections/content.tsx +111 -0
  23. package/templates/blank/src/sections/editorial.tsx +107 -0
  24. package/templates/blank/src/settings.ts +15 -0
  25. package/templates/blank/src/styles/theme.css +195 -0
  26. package/templates/blank/src/templates.ts +14 -0
  27. package/templates/blank/src/theme.tsx +76 -0
  28. package/templates/blank/template.json +24 -0
  29. package/templates/blank/templates/404.json +14 -0
  30. package/templates/blank/templates/account.json +14 -0
  31. package/templates/blank/templates/article.json +14 -0
  32. package/templates/blank/templates/blog.json +14 -0
  33. package/templates/blank/templates/cart.json +14 -0
  34. package/templates/blank/templates/collection.json +14 -0
  35. package/templates/blank/templates/index.json +36 -0
  36. package/templates/blank/templates/page.contact.json +18 -0
  37. package/templates/blank/templates/page.json +14 -0
  38. package/templates/blank/templates/product.json +18 -0
  39. package/templates/blank/templates/search.json +14 -0
  40. package/templates/blank/tests/theme.test.ts +49 -0
  41. package/templates/blank/tsconfig.json +17 -0
  42. package/templates/blank/vite.client.config.ts +17 -0
  43. package/templates/blank/vite.probe.config.ts +16 -0
  44. package/templates/blank/vite.server.config.ts +32 -0
  45. package/templates/blank/vitest.config.ts +9 -0
  46. package/templates/common/.claude/settings.json +8 -0
  47. package/templates/common/.codex/config.toml +4 -0
  48. package/templates/common/.gemini/settings.json +12 -0
  49. package/templates/common/.github/workflows/theme-check.yml +44 -0
  50. package/templates/common/.mcp.json +12 -0
  51. package/templates/common/.vscode/mcp.json +9 -0
  52. package/templates/common/_gitignore +9 -0
  53. package/templates/common/scripts/check.mjs +44 -0
  54. package/templates/common/scripts/dev.mjs +30 -0
  55. package/templates/common/tanqory.config.json +6 -0
  56. package/templates/nova/.gitattributes +3 -0
  57. package/templates/nova/.tanqory/contracts/LOCK.json +66 -0
  58. package/templates/nova/.tanqory/contracts/nova-01.json +1092 -0
  59. package/templates/nova/.tanqory/contracts/nova-12A.json +2674 -0
  60. package/templates/nova/.tanqory/contracts/nova-12B.json +2285 -0
  61. package/templates/nova/.tanqory/contracts/nova-12C.json +2848 -0
  62. package/templates/nova/.tanqory/contracts/nova-12D.json +3039 -0
  63. package/templates/nova/.tanqory/contracts/nova-12E.json +1473 -0
  64. package/templates/nova/.tanqory/contracts/nova-12F.json +1670 -0
  65. package/templates/nova/.tanqory/contracts/nova-12G.json +1661 -0
  66. package/templates/nova/.tanqory/contracts/nova-13.json +301 -0
  67. package/templates/nova/.tanqory/contracts/nova-14.json +172 -0
  68. package/templates/nova/.tanqory/legacy-settings.json +1121 -0
  69. package/templates/nova/README.md +150 -0
  70. package/templates/nova/_gitignore +28 -0
  71. package/templates/nova/assets/ai-tokens.css +6 -0
  72. package/templates/nova/assets/starter/starter-art-1200x800-0b773ac4.svg +1 -0
  73. package/templates/nova/assets/starter/starter-art-1200x900-59600290.svg +1 -0
  74. package/templates/nova/assets/starter/starter-art-1200x900-c549d496.svg +1 -0
  75. package/templates/nova/assets/starter/starter-art-1600x900-8f979fcb.svg +1 -0
  76. package/templates/nova/assets/starter/starter-art-2100x900-cbb71bfd.svg +1 -0
  77. package/templates/nova/config/legacy-settings.json +438 -0
  78. package/templates/nova/config/promotion-popup.schema.ts +29 -0
  79. package/templates/nova/config/settings.json +23 -0
  80. package/templates/nova/config/settings.schema.ts +231 -0
  81. package/templates/nova/entry.abi.tsx +27 -0
  82. package/templates/nova/groups/footer.json +39 -0
  83. package/templates/nova/groups/header.json +11 -0
  84. package/templates/nova/lib/abi/abi.css +41 -0
  85. package/templates/nova/lib/bundle-core.ts +35 -0
  86. package/templates/nova/lib/bundle.client.ts +15 -0
  87. package/templates/nova/lib/bundle.ts +15 -0
  88. package/templates/nova/lib/legacy-settings.ts +104 -0
  89. package/templates/nova/locales/ar.json +590 -0
  90. package/templates/nova/locales/en.json +590 -0
  91. package/templates/nova/locales/th.json +590 -0
  92. package/templates/nova/main.abi.tsx +12 -0
  93. package/templates/nova/package.json +44 -0
  94. package/templates/nova/scripts/abi/build.mjs +145 -0
  95. package/templates/nova/scripts/abi/check-content.mjs +99 -0
  96. package/templates/nova/scripts/abi/jcs.mjs +27 -0
  97. package/templates/nova/scripts/abi/local-host.mjs +294 -0
  98. package/templates/nova/scripts/abi/manifest.mjs +392 -0
  99. package/templates/nova/scripts/abi/starter-assets.mjs +32 -0
  100. package/templates/nova/scripts/compiler/checks/check-a11y.mjs +88 -0
  101. package/templates/nova/scripts/compiler/checks/check-layout-sanity.mjs +169 -0
  102. package/templates/nova/scripts/compiler/checks/check-page-headings.mjs +67 -0
  103. package/templates/nova/scripts/compiler/checks/check-product-jsonld.mjs +80 -0
  104. package/templates/nova/scripts/compiler/checks/theme-spec/check.mjs +88 -0
  105. package/templates/nova/scripts/compiler/contracts/extract.mjs +188 -0
  106. package/templates/nova/scripts/compiler/contracts/index.ts +125 -0
  107. package/templates/nova/scripts/compiler/contracts/validate.ts +347 -0
  108. package/templates/nova/scripts/compiler/manifest/gen-manifest.mjs +156 -0
  109. package/templates/nova/scripts/compiler/manifest/manifest-entry.ts +386 -0
  110. package/templates/nova/scripts/compiler/manifest/manifest-sections.ts +247 -0
  111. package/templates/nova/scripts/compiler/migrate/migrate-content.mjs +171 -0
  112. package/templates/nova/scripts/compiler/migrate/migrate-groups.mjs +159 -0
  113. package/templates/nova/scripts/parity/case-shim.ts +14 -0
  114. package/templates/nova/scripts/parity/emit.ts +206 -0
  115. package/templates/nova/scripts/parity/index.html +12 -0
  116. package/templates/nova/scripts/parity/main.tsx +23 -0
  117. package/templates/nova/scripts/parity/pages.tsx +123 -0
  118. package/templates/nova/scripts/parity/vite.config.ts +29 -0
  119. package/templates/nova/src/components/base/Accordion.css +20 -0
  120. package/templates/nova/src/components/base/Accordion.tsx +50 -0
  121. package/templates/nova/src/components/base/Badge.css +17 -0
  122. package/templates/nova/src/components/base/Badge.tsx +26 -0
  123. package/templates/nova/src/components/base/Button.css +73 -0
  124. package/templates/nova/src/components/base/Button.tsx +64 -0
  125. package/templates/nova/src/components/base/Checkbox.tsx +18 -0
  126. package/templates/nova/src/components/base/Chip.css +20 -0
  127. package/templates/nova/src/components/base/Chip.tsx +33 -0
  128. package/templates/nova/src/components/base/Divider.css +6 -0
  129. package/templates/nova/src/components/base/Divider.tsx +28 -0
  130. package/templates/nova/src/components/base/IconButton.css +34 -0
  131. package/templates/nova/src/components/base/IconButton.tsx +41 -0
  132. package/templates/nova/src/components/base/Input.css +21 -0
  133. package/templates/nova/src/components/base/Input.tsx +24 -0
  134. package/templates/nova/src/components/base/Link.css +15 -0
  135. package/templates/nova/src/components/base/Link.tsx +23 -0
  136. package/templates/nova/src/components/base/Radio.tsx +13 -0
  137. package/templates/nova/src/components/base/Select.css +18 -0
  138. package/templates/nova/src/components/base/Select.tsx +34 -0
  139. package/templates/nova/src/components/base/Skeleton.css +17 -0
  140. package/templates/nova/src/components/base/Skeleton.tsx +23 -0
  141. package/templates/nova/src/components/base/Spinner.css +13 -0
  142. package/templates/nova/src/components/base/Spinner.tsx +8 -0
  143. package/templates/nova/src/components/base/Switch.tsx +16 -0
  144. package/templates/nova/src/components/base/Tabs.css +24 -0
  145. package/templates/nova/src/components/base/Tabs.tsx +61 -0
  146. package/templates/nova/src/components/base/Textarea.css +1 -0
  147. package/templates/nova/src/components/base/Textarea.tsx +8 -0
  148. package/templates/nova/src/components/base/choice-props.ts +9 -0
  149. package/templates/nova/src/components/base/choice.css +69 -0
  150. package/templates/nova/src/components/commerce/AddToCart.css +4 -0
  151. package/templates/nova/src/components/commerce/AddToCart.tsx +77 -0
  152. package/templates/nova/src/components/commerce/CartLine.css +26 -0
  153. package/templates/nova/src/components/commerce/CartLine.tsx +82 -0
  154. package/templates/nova/src/components/commerce/CartSummary.css +23 -0
  155. package/templates/nova/src/components/commerce/CartSummary.tsx +120 -0
  156. package/templates/nova/src/components/commerce/CollectionCard.css +66 -0
  157. package/templates/nova/src/components/commerce/CollectionCard.tsx +57 -0
  158. package/templates/nova/src/components/commerce/ComparisonTable.css +33 -0
  159. package/templates/nova/src/components/commerce/ComparisonTable.tsx +113 -0
  160. package/templates/nova/src/components/commerce/EarnHint.css +3 -0
  161. package/templates/nova/src/components/commerce/EarnHint.tsx +31 -0
  162. package/templates/nova/src/components/commerce/Hotspot.css +16 -0
  163. package/templates/nova/src/components/commerce/Hotspot.tsx +64 -0
  164. package/templates/nova/src/components/commerce/ListingParts.css +79 -0
  165. package/templates/nova/src/components/commerce/ListingParts.tsx +211 -0
  166. package/templates/nova/src/components/commerce/LookItem.css +19 -0
  167. package/templates/nova/src/components/commerce/LookItem.tsx +41 -0
  168. package/templates/nova/src/components/commerce/MiniCartLine.css +20 -0
  169. package/templates/nova/src/components/commerce/MiniCartLine.tsx +93 -0
  170. package/templates/nova/src/components/commerce/PaymentMark.css +2 -0
  171. package/templates/nova/src/components/commerce/PaymentMark.tsx +55 -0
  172. package/templates/nova/src/components/commerce/Price.css +27 -0
  173. package/templates/nova/src/components/commerce/Price.tsx +55 -0
  174. package/templates/nova/src/components/commerce/ProductCard.css +66 -0
  175. package/templates/nova/src/components/commerce/ProductCard.tsx +117 -0
  176. package/templates/nova/src/components/commerce/ProductGallery.css +72 -0
  177. package/templates/nova/src/components/commerce/ProductGallery.tsx +98 -0
  178. package/templates/nova/src/components/commerce/QuantitySelector.css +23 -0
  179. package/templates/nova/src/components/commerce/QuantitySelector.tsx +45 -0
  180. package/templates/nova/src/components/commerce/Reviews.css +34 -0
  181. package/templates/nova/src/components/commerce/Reviews.tsx +112 -0
  182. package/templates/nova/src/components/commerce/Stars.css +7 -0
  183. package/templates/nova/src/components/commerce/Stars.tsx +38 -0
  184. package/templates/nova/src/components/commerce/VariantPicker.css +31 -0
  185. package/templates/nova/src/components/commerce/VariantPicker.tsx +122 -0
  186. package/templates/nova/src/components/commerce/blocks/Badge.css +6 -0
  187. package/templates/nova/src/components/commerce/blocks/Badge.tsx +33 -0
  188. package/templates/nova/src/components/commerce/blocks/PaymentIcons.tsx +43 -0
  189. package/templates/nova/src/components/commerce/blocks/Swatches.tsx +17 -0
  190. package/templates/nova/src/components/commerce/blocks/payment.css +2 -0
  191. package/templates/nova/src/components/commerce/cart-limit.ts +11 -0
  192. package/templates/nova/src/components/commerce/money.ts +34 -0
  193. package/templates/nova/src/components/commerce/swatch.ts +54 -0
  194. package/templates/nova/src/components/content/ArticleCard.css +22 -0
  195. package/templates/nova/src/components/content/ArticleCard.tsx +53 -0
  196. package/templates/nova/src/components/content/BeforeAfterSlider.css +38 -0
  197. package/templates/nova/src/components/content/BeforeAfterSlider.tsx +87 -0
  198. package/templates/nova/src/components/content/CardRow.css +80 -0
  199. package/templates/nova/src/components/content/CardRow.tsx +140 -0
  200. package/templates/nova/src/components/content/CarouselControls.css +61 -0
  201. package/templates/nova/src/components/content/CarouselControls.tsx +90 -0
  202. package/templates/nova/src/components/content/ContentsNav.css +16 -0
  203. package/templates/nova/src/components/content/ContentsNav.tsx +55 -0
  204. package/templates/nova/src/components/content/Countdown.css +24 -0
  205. package/templates/nova/src/components/content/Countdown.tsx +115 -0
  206. package/templates/nova/src/components/content/HeroCopy.css +92 -0
  207. package/templates/nova/src/components/content/HeroCopy.tsx +148 -0
  208. package/templates/nova/src/components/content/HeroFrame.css +75 -0
  209. package/templates/nova/src/components/content/HeroFrame.tsx +49 -0
  210. package/templates/nova/src/components/content/HeroMedia.css +40 -0
  211. package/templates/nova/src/components/content/HeroMedia.tsx +170 -0
  212. package/templates/nova/src/components/content/LongForm.css +40 -0
  213. package/templates/nova/src/components/content/LongForm.tsx +56 -0
  214. package/templates/nova/src/components/content/MediaFrame.css +5 -0
  215. package/templates/nova/src/components/content/MediaFrame.tsx +52 -0
  216. package/templates/nova/src/components/content/RichText.tsx +30 -0
  217. package/templates/nova/src/components/content/SectionHead.css +8 -0
  218. package/templates/nova/src/components/content/SectionHead.tsx +34 -0
  219. package/templates/nova/src/components/content/ShareButtons.css +2 -0
  220. package/templates/nova/src/components/content/ShareButtons.tsx +40 -0
  221. package/templates/nova/src/components/content/StoreCard.css +14 -0
  222. package/templates/nova/src/components/content/StoreCard.tsx +60 -0
  223. package/templates/nova/src/components/content/TestimonialCard.css +36 -0
  224. package/templates/nova/src/components/content/TestimonialCard.tsx +102 -0
  225. package/templates/nova/src/components/content/UgcTile.css +8 -0
  226. package/templates/nova/src/components/content/UgcTile.tsx +33 -0
  227. package/templates/nova/src/components/content/blocks/Button.tsx +36 -0
  228. package/templates/nova/src/components/content/blocks/Column.tsx +30 -0
  229. package/templates/nova/src/components/content/blocks/Heading.tsx +31 -0
  230. package/templates/nova/src/components/content/blocks/Icon.tsx +25 -0
  231. package/templates/nova/src/components/content/blocks/Image.tsx +42 -0
  232. package/templates/nova/src/components/content/blocks/Text.tsx +26 -0
  233. package/templates/nova/src/components/content/blocks/Video.tsx +44 -0
  234. package/templates/nova/src/components/content/blocks/content.css +25 -0
  235. package/templates/nova/src/components/content/blocks/shared.ts +5 -0
  236. package/templates/nova/src/components/forms/ContactForm.css +19 -0
  237. package/templates/nova/src/components/forms/ContactForm.tsx +139 -0
  238. package/templates/nova/src/components/forms/FormField.css +15 -0
  239. package/templates/nova/src/components/forms/FormField.tsx +77 -0
  240. package/templates/nova/src/components/forms/GatewayFields.tsx +79 -0
  241. package/templates/nova/src/components/forms/NewsletterForm.css +9 -0
  242. package/templates/nova/src/components/forms/NewsletterForm.tsx +141 -0
  243. package/templates/nova/src/components/forms/SearchField.css +7 -0
  244. package/templates/nova/src/components/forms/SearchField.tsx +68 -0
  245. package/templates/nova/src/components/forms/blocks/Newsletter.css +5 -0
  246. package/templates/nova/src/components/forms/blocks/Newsletter.tsx +71 -0
  247. package/templates/nova/src/components/forms/validate.ts +4 -0
  248. package/templates/nova/src/components/listing/ProductListing.tsx +200 -0
  249. package/templates/nova/src/components/navigation/BrandLogo.tsx +35 -0
  250. package/templates/nova/src/components/navigation/Breadcrumb.css +16 -0
  251. package/templates/nova/src/components/navigation/Breadcrumb.tsx +59 -0
  252. package/templates/nova/src/components/navigation/LineAddFriend.css +5 -0
  253. package/templates/nova/src/components/navigation/LineAddFriend.tsx +42 -0
  254. package/templates/nova/src/components/navigation/LocaleSelector.css +33 -0
  255. package/templates/nova/src/components/navigation/LocaleSelector.tsx +64 -0
  256. package/templates/nova/src/components/navigation/Menu.tsx +26 -0
  257. package/templates/nova/src/components/navigation/Pagination.css +24 -0
  258. package/templates/nova/src/components/navigation/Pagination.tsx +74 -0
  259. package/templates/nova/src/components/navigation/RecoveryLinks.css +8 -0
  260. package/templates/nova/src/components/navigation/RecoveryLinks.tsx +23 -0
  261. package/templates/nova/src/components/navigation/SocialLinks.css +5 -0
  262. package/templates/nova/src/components/navigation/SocialLinks.tsx +51 -0
  263. package/templates/nova/src/components/navigation/blocks/SocialLinks.tsx +23 -0
  264. package/templates/nova/src/components/overlays/AccountMenu.css +11 -0
  265. package/templates/nova/src/components/overlays/AccountMenu.tsx +91 -0
  266. package/templates/nova/src/components/overlays/CartDrawer.css +17 -0
  267. package/templates/nova/src/components/overlays/CartDrawer.tsx +118 -0
  268. package/templates/nova/src/components/overlays/ConfirmationDialog.tsx +49 -0
  269. package/templates/nova/src/components/overlays/CookieConsent.css +56 -0
  270. package/templates/nova/src/components/overlays/CookieConsent.tsx +170 -0
  271. package/templates/nova/src/components/overlays/Drawer.css +38 -0
  272. package/templates/nova/src/components/overlays/Drawer.tsx +66 -0
  273. package/templates/nova/src/components/overlays/FriendOfferPanel.tsx +79 -0
  274. package/templates/nova/src/components/overlays/MarketSelector.css +111 -0
  275. package/templates/nova/src/components/overlays/MarketSelector.tsx +450 -0
  276. package/templates/nova/src/components/overlays/Menu.css +9 -0
  277. package/templates/nova/src/components/overlays/Menu.tsx +124 -0
  278. package/templates/nova/src/components/overlays/MobileNavDrawer.css +28 -0
  279. package/templates/nova/src/components/overlays/MobileNavDrawer.tsx +166 -0
  280. package/templates/nova/src/components/overlays/Modal.css +52 -0
  281. package/templates/nova/src/components/overlays/Modal.tsx +88 -0
  282. package/templates/nova/src/components/overlays/Popover.css +4 -0
  283. package/templates/nova/src/components/overlays/Popover.tsx +115 -0
  284. package/templates/nova/src/components/overlays/ProductLightbox.css +20 -0
  285. package/templates/nova/src/components/overlays/ProductLightbox.tsx +70 -0
  286. package/templates/nova/src/components/overlays/PromoPopup.css +32 -0
  287. package/templates/nova/src/components/overlays/PromoPopup.tsx +198 -0
  288. package/templates/nova/src/components/overlays/QuickView.css +31 -0
  289. package/templates/nova/src/components/overlays/QuickView.tsx +127 -0
  290. package/templates/nova/src/components/overlays/ReferralPanel.css +16 -0
  291. package/templates/nova/src/components/overlays/ReferralPanel.tsx +138 -0
  292. package/templates/nova/src/components/overlays/RewardsLauncher.css +22 -0
  293. package/templates/nova/src/components/overlays/RewardsLauncher.tsx +270 -0
  294. package/templates/nova/src/components/overlays/SearchModal.css +48 -0
  295. package/templates/nova/src/components/overlays/SearchModal.tsx +302 -0
  296. package/templates/nova/src/components/overlays/Sheet.css +16 -0
  297. package/templates/nova/src/components/overlays/Sheet.tsx +48 -0
  298. package/templates/nova/src/components/overlays/SizeGuide.css +8 -0
  299. package/templates/nova/src/components/overlays/SizeGuide.tsx +74 -0
  300. package/templates/nova/src/components/overlays/Toast.css +20 -0
  301. package/templates/nova/src/components/overlays/Toast.tsx +164 -0
  302. package/templates/nova/src/components/overlays/Tooltip.css +6 -0
  303. package/templates/nova/src/components/overlays/Tooltip.tsx +59 -0
  304. package/templates/nova/src/components/overlays/use-dialog.ts +74 -0
  305. package/templates/nova/src/components/section/shared.css +42 -0
  306. package/templates/nova/src/components/section/shared.ts +120 -0
  307. package/templates/nova/src/components/section/shell.tsx +29 -0
  308. package/templates/nova/src/components/states/Alert.css +10 -0
  309. package/templates/nova/src/components/states/Alert.tsx +50 -0
  310. package/templates/nova/src/components/states/EditorFrame.css +31 -0
  311. package/templates/nova/src/components/states/EditorFrame.tsx +43 -0
  312. package/templates/nova/src/components/states/MediaState.css +1 -0
  313. package/templates/nova/src/components/states/MediaState.tsx +49 -0
  314. package/templates/nova/src/components/states/OfflineNotice.css +4 -0
  315. package/templates/nova/src/components/states/OfflineNotice.tsx +24 -0
  316. package/templates/nova/src/components/states/SkeletonLayout.css +22 -0
  317. package/templates/nova/src/components/states/SkeletonLayout.tsx +104 -0
  318. package/templates/nova/src/components/states/StateBlock.css +22 -0
  319. package/templates/nova/src/components/states/StateBlock.tsx +39 -0
  320. package/templates/nova/src/components/states/media/MediaError.css +6 -0
  321. package/templates/nova/src/components/states/media/MediaError.tsx +49 -0
  322. package/templates/nova/src/components/states/media/MediaPlaceholder.css +24 -0
  323. package/templates/nova/src/components/states/media/MediaPlaceholder.tsx +101 -0
  324. package/templates/nova/src/components/states/media/placeholder-art.ts +142 -0
  325. package/templates/nova/src/layouts/Main.tsx +80 -0
  326. package/templates/nova/src/sections/Account/Account.tsx +109 -0
  327. package/templates/nova/src/sections/Account/index.ts +22 -0
  328. package/templates/nova/src/sections/Account/presets.ts +6 -0
  329. package/templates/nova/src/sections/Account/schema.ts +29 -0
  330. package/templates/nova/src/sections/AnnouncementBar/AnnouncementBar.css +37 -0
  331. package/templates/nova/src/sections/AnnouncementBar/AnnouncementBar.tsx +151 -0
  332. package/templates/nova/src/sections/AnnouncementBar/index.ts +20 -0
  333. package/templates/nova/src/sections/AnnouncementBar/presets.ts +10 -0
  334. package/templates/nova/src/sections/AnnouncementBar/schema.ts +26 -0
  335. package/templates/nova/src/sections/ArticleBody/ArticleBody.tsx +129 -0
  336. package/templates/nova/src/sections/ArticleBody/index.ts +21 -0
  337. package/templates/nova/src/sections/ArticleBody/presets.ts +10 -0
  338. package/templates/nova/src/sections/ArticleBody/schema.ts +40 -0
  339. package/templates/nova/src/sections/BeforeAfter/BeforeAfter.tsx +47 -0
  340. package/templates/nova/src/sections/BeforeAfter/blocks/Comparison.tsx +20 -0
  341. package/templates/nova/src/sections/BeforeAfter/index.ts +28 -0
  342. package/templates/nova/src/sections/BeforeAfter/presets.ts +10 -0
  343. package/templates/nova/src/sections/BeforeAfter/schema.ts +34 -0
  344. package/templates/nova/src/sections/BlogPosts/BlogPosts.tsx +106 -0
  345. package/templates/nova/src/sections/BlogPosts/index.ts +30 -0
  346. package/templates/nova/src/sections/BlogPosts/presets.ts +10 -0
  347. package/templates/nova/src/sections/BlogPosts/schema.ts +42 -0
  348. package/templates/nova/src/sections/Breadcrumbs/Breadcrumbs.css +10 -0
  349. package/templates/nova/src/sections/Breadcrumbs/Breadcrumbs.tsx +41 -0
  350. package/templates/nova/src/sections/Breadcrumbs/index.ts +20 -0
  351. package/templates/nova/src/sections/Breadcrumbs/presets.ts +6 -0
  352. package/templates/nova/src/sections/Breadcrumbs/schema.ts +24 -0
  353. package/templates/nova/src/sections/CartItems/CartItems.tsx +170 -0
  354. package/templates/nova/src/sections/CartItems/blocks/CartBadge.tsx +14 -0
  355. package/templates/nova/src/sections/CartItems/index.ts +37 -0
  356. package/templates/nova/src/sections/CartItems/presets.ts +8 -0
  357. package/templates/nova/src/sections/CartItems/schema.ts +34 -0
  358. package/templates/nova/src/sections/CollectionBanner/CollectionBanner.tsx +120 -0
  359. package/templates/nova/src/sections/CollectionBanner/index.ts +25 -0
  360. package/templates/nova/src/sections/CollectionBanner/presets.ts +10 -0
  361. package/templates/nova/src/sections/CollectionBanner/schema.ts +37 -0
  362. package/templates/nova/src/sections/CollectionLinks/CollectionLinks.tsx +47 -0
  363. package/templates/nova/src/sections/CollectionLinks/index.ts +20 -0
  364. package/templates/nova/src/sections/CollectionLinks/presets.ts +8 -0
  365. package/templates/nova/src/sections/CollectionLinks/schema.ts +33 -0
  366. package/templates/nova/src/sections/CollectionList/CollectionList.tsx +89 -0
  367. package/templates/nova/src/sections/CollectionList/blocks/CollectionItem.tsx +25 -0
  368. package/templates/nova/src/sections/CollectionList/index.ts +33 -0
  369. package/templates/nova/src/sections/CollectionList/presets.ts +10 -0
  370. package/templates/nova/src/sections/CollectionList/schema.ts +41 -0
  371. package/templates/nova/src/sections/ComparisonTable/ComparisonTable.tsx +52 -0
  372. package/templates/nova/src/sections/ComparisonTable/blocks/CompareItem.tsx +26 -0
  373. package/templates/nova/src/sections/ComparisonTable/index.ts +31 -0
  374. package/templates/nova/src/sections/ComparisonTable/presets.ts +8 -0
  375. package/templates/nova/src/sections/ComparisonTable/schema.ts +31 -0
  376. package/templates/nova/src/sections/ContactForm/ContactForm.tsx +83 -0
  377. package/templates/nova/src/sections/ContactForm/blocks/ContactDetail.tsx +13 -0
  378. package/templates/nova/src/sections/ContactForm/index.ts +29 -0
  379. package/templates/nova/src/sections/ContactForm/presets.ts +8 -0
  380. package/templates/nova/src/sections/ContactForm/schema.ts +34 -0
  381. package/templates/nova/src/sections/Divider/Divider.css +17 -0
  382. package/templates/nova/src/sections/Divider/Divider.tsx +21 -0
  383. package/templates/nova/src/sections/Divider/index.ts +20 -0
  384. package/templates/nova/src/sections/Divider/presets.ts +10 -0
  385. package/templates/nova/src/sections/Divider/schema.ts +24 -0
  386. package/templates/nova/src/sections/Faq/Faq.tsx +51 -0
  387. package/templates/nova/src/sections/Faq/blocks/FaqItem.tsx +13 -0
  388. package/templates/nova/src/sections/Faq/index.ts +28 -0
  389. package/templates/nova/src/sections/Faq/presets.ts +8 -0
  390. package/templates/nova/src/sections/Faq/schema.ts +32 -0
  391. package/templates/nova/src/sections/FeatureGridBlocks/FeatureGridBlocks.tsx +90 -0
  392. package/templates/nova/src/sections/FeatureGridBlocks/index.ts +35 -0
  393. package/templates/nova/src/sections/FeatureGridBlocks/presets.ts +8 -0
  394. package/templates/nova/src/sections/FeatureGridBlocks/schema.ts +35 -0
  395. package/templates/nova/src/sections/FeatureHighlights/FeatureHighlights.tsx +56 -0
  396. package/templates/nova/src/sections/FeatureHighlights/index.ts +20 -0
  397. package/templates/nova/src/sections/FeatureHighlights/presets.ts +6 -0
  398. package/templates/nova/src/sections/FeatureHighlights/schema.ts +27 -0
  399. package/templates/nova/src/sections/FeaturedCollection/FeaturedCollection.tsx +67 -0
  400. package/templates/nova/src/sections/FeaturedCollection/index.ts +21 -0
  401. package/templates/nova/src/sections/FeaturedCollection/presets.ts +10 -0
  402. package/templates/nova/src/sections/FeaturedCollection/schema.ts +39 -0
  403. package/templates/nova/src/sections/FeaturedProduct/FeaturedProduct.tsx +107 -0
  404. package/templates/nova/src/sections/FeaturedProduct/index.ts +23 -0
  405. package/templates/nova/src/sections/FeaturedProduct/presets.ts +8 -0
  406. package/templates/nova/src/sections/FeaturedProduct/schema.ts +35 -0
  407. package/templates/nova/src/sections/Footer/Footer.css +81 -0
  408. package/templates/nova/src/sections/Footer/Footer.tsx +256 -0
  409. package/templates/nova/src/sections/Footer/blocks/FooterBrand.tsx +40 -0
  410. package/templates/nova/src/sections/Footer/blocks/FooterLine.tsx +11 -0
  411. package/templates/nova/src/sections/Footer/blocks/FooterMenu.tsx +14 -0
  412. package/templates/nova/src/sections/Footer/blocks/FooterText.tsx +8 -0
  413. package/templates/nova/src/sections/Footer/blocks/footer.css +7 -0
  414. package/templates/nova/src/sections/Footer/index.ts +38 -0
  415. package/templates/nova/src/sections/Footer/presets.ts +10 -0
  416. package/templates/nova/src/sections/Footer/schema.ts +27 -0
  417. package/templates/nova/src/sections/Group/Group.css +59 -0
  418. package/templates/nova/src/sections/Group/Group.tsx +167 -0
  419. package/templates/nova/src/sections/Group/blocks/Accordion.tsx +26 -0
  420. package/templates/nova/src/sections/Group/blocks/JumboText.tsx +20 -0
  421. package/templates/nova/src/sections/Group/blocks/Spacer.tsx +16 -0
  422. package/templates/nova/src/sections/Group/index.ts +36 -0
  423. package/templates/nova/src/sections/Group/presets.ts +12 -0
  424. package/templates/nova/src/sections/Group/schema.ts +35 -0
  425. package/templates/nova/src/sections/Header/Header.css +185 -0
  426. package/templates/nova/src/sections/Header/Header.tsx +478 -0
  427. package/templates/nova/src/sections/Header/index.ts +32 -0
  428. package/templates/nova/src/sections/Header/presets.ts +10 -0
  429. package/templates/nova/src/sections/Header/schema.ts +32 -0
  430. package/templates/nova/src/sections/Hero/Hero.tsx +128 -0
  431. package/templates/nova/src/sections/Hero/index.ts +24 -0
  432. package/templates/nova/src/sections/Hero/presets.ts +16 -0
  433. package/templates/nova/src/sections/Hero/schema.ts +56 -0
  434. package/templates/nova/src/sections/ImageGallery/ImageGallery.tsx +86 -0
  435. package/templates/nova/src/sections/ImageGallery/index.ts +28 -0
  436. package/templates/nova/src/sections/ImageGallery/presets.ts +10 -0
  437. package/templates/nova/src/sections/ImageGallery/schema.ts +34 -0
  438. package/templates/nova/src/sections/ImageWithText/ImageWithText.tsx +62 -0
  439. package/templates/nova/src/sections/ImageWithText/index.ts +23 -0
  440. package/templates/nova/src/sections/ImageWithText/presets.ts +12 -0
  441. package/templates/nova/src/sections/ImageWithText/schema.ts +45 -0
  442. package/templates/nova/src/sections/LineAddFriend/LineAddFriend.tsx +25 -0
  443. package/templates/nova/src/sections/LineAddFriend/index.ts +20 -0
  444. package/templates/nova/src/sections/LineAddFriend/presets.ts +3 -0
  445. package/templates/nova/src/sections/LineAddFriend/schema.ts +25 -0
  446. package/templates/nova/src/sections/LogoList/LogoList.tsx +40 -0
  447. package/templates/nova/src/sections/LogoList/blocks/Logo.tsx +18 -0
  448. package/templates/nova/src/sections/LogoList/index.ts +29 -0
  449. package/templates/nova/src/sections/LogoList/presets.ts +8 -0
  450. package/templates/nova/src/sections/LogoList/schema.ts +29 -0
  451. package/templates/nova/src/sections/MainCollection/MainCollection.tsx +51 -0
  452. package/templates/nova/src/sections/MainCollection/index.ts +26 -0
  453. package/templates/nova/src/sections/MainCollection/presets.ts +10 -0
  454. package/templates/nova/src/sections/MainCollection/schema.ts +29 -0
  455. package/templates/nova/src/sections/Marquee/Marquee.tsx +114 -0
  456. package/templates/nova/src/sections/Marquee/index.ts +20 -0
  457. package/templates/nova/src/sections/Marquee/presets.ts +10 -0
  458. package/templates/nova/src/sections/Marquee/schema.ts +23 -0
  459. package/templates/nova/src/sections/Multicolumn/Multicolumn.tsx +56 -0
  460. package/templates/nova/src/sections/Multicolumn/index.ts +25 -0
  461. package/templates/nova/src/sections/Multicolumn/presets.ts +8 -0
  462. package/templates/nova/src/sections/Multicolumn/schema.ts +34 -0
  463. package/templates/nova/src/sections/Newsletter/Newsletter.tsx +64 -0
  464. package/templates/nova/src/sections/Newsletter/index.ts +20 -0
  465. package/templates/nova/src/sections/Newsletter/presets.ts +10 -0
  466. package/templates/nova/src/sections/Newsletter/schema.ts +36 -0
  467. package/templates/nova/src/sections/NotFound/NotFound.tsx +41 -0
  468. package/templates/nova/src/sections/NotFound/index.ts +20 -0
  469. package/templates/nova/src/sections/NotFound/presets.ts +6 -0
  470. package/templates/nova/src/sections/NotFound/schema.ts +30 -0
  471. package/templates/nova/src/sections/PageBody/PageBody.tsx +35 -0
  472. package/templates/nova/src/sections/PageBody/index.ts +21 -0
  473. package/templates/nova/src/sections/PageBody/presets.ts +6 -0
  474. package/templates/nova/src/sections/PageBody/schema.ts +29 -0
  475. package/templates/nova/src/sections/PolicyPage/PolicyPage.tsx +62 -0
  476. package/templates/nova/src/sections/PolicyPage/index.ts +23 -0
  477. package/templates/nova/src/sections/PolicyPage/presets.ts +6 -0
  478. package/templates/nova/src/sections/PolicyPage/schema.ts +27 -0
  479. package/templates/nova/src/sections/ProductDetails/ProductDetails.tsx +269 -0
  480. package/templates/nova/src/sections/ProductDetails/blocks/AddToCartBlock.tsx +15 -0
  481. package/templates/nova/src/sections/ProductDetails/blocks/ProductDescription.tsx +15 -0
  482. package/templates/nova/src/sections/ProductDetails/blocks/ProductInventory.tsx +38 -0
  483. package/templates/nova/src/sections/ProductDetails/blocks/ProductPrice.tsx +19 -0
  484. package/templates/nova/src/sections/ProductDetails/blocks/ProductSku.tsx +15 -0
  485. package/templates/nova/src/sections/ProductDetails/blocks/ProductTitle.tsx +17 -0
  486. package/templates/nova/src/sections/ProductDetails/blocks/Quantity.tsx +14 -0
  487. package/templates/nova/src/sections/ProductDetails/blocks/Tab.tsx +23 -0
  488. package/templates/nova/src/sections/ProductDetails/blocks/VariantPickerBlock.tsx +44 -0
  489. package/templates/nova/src/sections/ProductDetails/blocks/commerce-blocks.css +13 -0
  490. package/templates/nova/src/sections/ProductDetails/index.ts +44 -0
  491. package/templates/nova/src/sections/ProductDetails/presets.ts +10 -0
  492. package/templates/nova/src/sections/ProductDetails/schema.ts +44 -0
  493. package/templates/nova/src/sections/ProductGrid/ProductGrid.tsx +38 -0
  494. package/templates/nova/src/sections/ProductGrid/index.ts +25 -0
  495. package/templates/nova/src/sections/ProductGrid/presets.ts +6 -0
  496. package/templates/nova/src/sections/ProductGrid/schema.ts +37 -0
  497. package/templates/nova/src/sections/ProductRecommendations/ProductRecommendations.tsx +45 -0
  498. package/templates/nova/src/sections/ProductRecommendations/index.ts +20 -0
  499. package/templates/nova/src/sections/ProductRecommendations/presets.ts +8 -0
  500. package/templates/nova/src/sections/ProductRecommendations/schema.ts +28 -0
  501. package/templates/nova/src/sections/ProductReviews/ProductReviews.tsx +136 -0
  502. package/templates/nova/src/sections/ProductReviews/index.ts +22 -0
  503. package/templates/nova/src/sections/ProductReviews/presets.ts +6 -0
  504. package/templates/nova/src/sections/ProductReviews/schema.ts +31 -0
  505. package/templates/nova/src/sections/PromoBanner/PromoBanner.tsx +78 -0
  506. package/templates/nova/src/sections/PromoBanner/index.ts +20 -0
  507. package/templates/nova/src/sections/PromoBanner/presets.ts +10 -0
  508. package/templates/nova/src/sections/PromoBanner/schema.ts +38 -0
  509. package/templates/nova/src/sections/RecentlyViewed/RecentlyViewed.tsx +53 -0
  510. package/templates/nova/src/sections/RecentlyViewed/index.ts +20 -0
  511. package/templates/nova/src/sections/RecentlyViewed/presets.ts +6 -0
  512. package/templates/nova/src/sections/RecentlyViewed/schema.ts +26 -0
  513. package/templates/nova/src/sections/RichText/RichText.tsx +38 -0
  514. package/templates/nova/src/sections/RichText/index.ts +20 -0
  515. package/templates/nova/src/sections/RichText/presets.ts +10 -0
  516. package/templates/nova/src/sections/RichText/schema.ts +37 -0
  517. package/templates/nova/src/sections/SearchResults/SearchResults.tsx +112 -0
  518. package/templates/nova/src/sections/SearchResults/index.ts +26 -0
  519. package/templates/nova/src/sections/SearchResults/presets.ts +8 -0
  520. package/templates/nova/src/sections/SearchResults/schema.ts +30 -0
  521. package/templates/nova/src/sections/ShopTheLook/ShopTheLook.tsx +112 -0
  522. package/templates/nova/src/sections/ShopTheLook/blocks/Hotspot.tsx +22 -0
  523. package/templates/nova/src/sections/ShopTheLook/index.ts +33 -0
  524. package/templates/nova/src/sections/ShopTheLook/presets.ts +8 -0
  525. package/templates/nova/src/sections/ShopTheLook/schema.ts +31 -0
  526. package/templates/nova/src/sections/Slideshow/Slideshow.tsx +199 -0
  527. package/templates/nova/src/sections/Slideshow/blocks/Slide.tsx +93 -0
  528. package/templates/nova/src/sections/Slideshow/index.ts +30 -0
  529. package/templates/nova/src/sections/Slideshow/presets.ts +10 -0
  530. package/templates/nova/src/sections/Slideshow/schema.ts +36 -0
  531. package/templates/nova/src/sections/StoreLocator/StoreLocator.tsx +66 -0
  532. package/templates/nova/src/sections/StoreLocator/index.ts +20 -0
  533. package/templates/nova/src/sections/StoreLocator/presets.ts +6 -0
  534. package/templates/nova/src/sections/StoreLocator/schema.ts +34 -0
  535. package/templates/nova/src/sections/TeamFounders/TeamFounders.tsx +74 -0
  536. package/templates/nova/src/sections/TeamFounders/blocks/Person.tsx +28 -0
  537. package/templates/nova/src/sections/TeamFounders/index.ts +28 -0
  538. package/templates/nova/src/sections/TeamFounders/presets.ts +10 -0
  539. package/templates/nova/src/sections/TeamFounders/schema.ts +36 -0
  540. package/templates/nova/src/sections/Testimonials/Testimonials.tsx +83 -0
  541. package/templates/nova/src/sections/Testimonials/blocks/Testimonial.tsx +30 -0
  542. package/templates/nova/src/sections/Testimonials/index.ts +29 -0
  543. package/templates/nova/src/sections/Testimonials/presets.ts +10 -0
  544. package/templates/nova/src/sections/Testimonials/schema.ts +35 -0
  545. package/templates/nova/src/sections/Timeline/Timeline.tsx +43 -0
  546. package/templates/nova/src/sections/Timeline/blocks/Step.tsx +19 -0
  547. package/templates/nova/src/sections/Timeline/index.ts +28 -0
  548. package/templates/nova/src/sections/Timeline/presets.ts +8 -0
  549. package/templates/nova/src/sections/Timeline/schema.ts +31 -0
  550. package/templates/nova/src/sections/TrustBadges/TrustBadges.tsx +42 -0
  551. package/templates/nova/src/sections/TrustBadges/index.ts +29 -0
  552. package/templates/nova/src/sections/TrustBadges/presets.ts +8 -0
  553. package/templates/nova/src/sections/TrustBadges/schema.ts +29 -0
  554. package/templates/nova/src/sections/UgcGallery/UgcGallery.tsx +58 -0
  555. package/templates/nova/src/sections/UgcGallery/blocks/UgcImage.tsx +27 -0
  556. package/templates/nova/src/sections/UgcGallery/index.ts +31 -0
  557. package/templates/nova/src/sections/UgcGallery/presets.ts +8 -0
  558. package/templates/nova/src/sections/UgcGallery/schema.ts +35 -0
  559. package/templates/nova/src/sections/VideoHero/VideoHero.tsx +75 -0
  560. package/templates/nova/src/sections/VideoHero/index.ts +22 -0
  561. package/templates/nova/src/sections/VideoHero/presets.ts +8 -0
  562. package/templates/nova/src/sections/VideoHero/schema.ts +43 -0
  563. package/templates/nova/src/sections/data.ts +115 -0
  564. package/templates/nova/src/sections/shipped-copy.ts +25 -0
  565. package/templates/nova/src/sections/starter.ts +49 -0
  566. package/templates/nova/src/styles/Container.css +6 -0
  567. package/templates/nova/src/styles/Container.tsx +13 -0
  568. package/templates/nova/src/styles/Icon.css +2 -0
  569. package/templates/nova/src/styles/Icon.tsx +101 -0
  570. package/templates/nova/src/styles/NovaProvider.tsx +91 -0
  571. package/templates/nova/src/styles/base.css +142 -0
  572. package/templates/nova/src/styles/blocks.css +6 -0
  573. package/templates/nova/src/styles/fonts/OFL.txt +93 -0
  574. package/templates/nova/src/styles/fonts/geist-cyrillic-ext.woff2 +0 -0
  575. package/templates/nova/src/styles/fonts/geist-cyrillic.woff2 +0 -0
  576. package/templates/nova/src/styles/fonts/geist-latin-ext.woff2 +0 -0
  577. package/templates/nova/src/styles/fonts/geist-latin.woff2 +0 -0
  578. package/templates/nova/src/styles/fonts/geist-mono-cyrillic-ext.woff2 +0 -0
  579. package/templates/nova/src/styles/fonts/geist-mono-cyrillic.woff2 +0 -0
  580. package/templates/nova/src/styles/fonts/geist-mono-latin-ext.woff2 +0 -0
  581. package/templates/nova/src/styles/fonts/geist-mono-latin.woff2 +0 -0
  582. package/templates/nova/src/styles/fonts/geist-mono-vietnamese.woff2 +0 -0
  583. package/templates/nova/src/styles/fonts/geist-vietnamese.woff2 +0 -0
  584. package/templates/nova/src/styles/fonts.css +95 -0
  585. package/templates/nova/src/styles/global-settings.css +88 -0
  586. package/templates/nova/src/styles/index.css +6 -0
  587. package/templates/nova/src/styles/motion.ts +53 -0
  588. package/templates/nova/src/styles/scroll.ts +24 -0
  589. package/templates/nova/src/styles/sections/commerce.css +147 -0
  590. package/templates/nova/src/styles/sections/content.css +304 -0
  591. package/templates/nova/src/styles/sections/editorial.css +131 -0
  592. package/templates/nova/src/styles/sections/promotion.css +336 -0
  593. package/templates/nova/src/styles/sections/trust.css +117 -0
  594. package/templates/nova/src/styles/sections/utility.css +190 -0
  595. package/templates/nova/src/styles/strings.ts +492 -0
  596. package/templates/nova/src/styles/text.ts +4 -0
  597. package/templates/nova/src/styles/theme.ts +171 -0
  598. package/templates/nova/src/styles/tokens.css +600 -0
  599. package/templates/nova/src/styles/viewport.ts +23 -0
  600. package/templates/nova/template.json +94 -0
  601. package/templates/nova/templates/404.json +30 -0
  602. package/templates/nova/templates/account.json +15 -0
  603. package/templates/nova/templates/article.json +41 -0
  604. package/templates/nova/templates/article.longform.json +17 -0
  605. package/templates/nova/templates/blog.featured.json +22 -0
  606. package/templates/nova/templates/blog.json +29 -0
  607. package/templates/nova/templates/cart.json +67 -0
  608. package/templates/nova/templates/collection.featured.json +43 -0
  609. package/templates/nova/templates/collection.json +30 -0
  610. package/templates/nova/templates/contact.json +65 -0
  611. package/templates/nova/templates/index.json +167 -0
  612. package/templates/nova/templates/list-collections.json +19 -0
  613. package/templates/nova/templates/page.about.json +134 -0
  614. package/templates/nova/templates/page.contact.json +27 -0
  615. package/templates/nova/templates/page.json +27 -0
  616. package/templates/nova/templates/policy.json +23 -0
  617. package/templates/nova/templates/product.bundle.json +145 -0
  618. package/templates/nova/templates/product.json +119 -0
  619. package/templates/nova/templates/search.json +17 -0
  620. package/templates/nova/tests/abi-ai-contract.test.ts +99 -0
  621. package/templates/nova/tests/abi-forms.test.tsx +269 -0
  622. package/templates/nova/tests/abi-inventory.test.tsx +197 -0
  623. package/templates/nova/tests/abi-legacy-settings.test.tsx +105 -0
  624. package/templates/nova/tests/abi-opt-in.test.tsx +100 -0
  625. package/templates/nova/tests/abi-parity.test.tsx +95 -0
  626. package/templates/nova/tests/abi-r5-store-settings.test.tsx +337 -0
  627. package/templates/nova/tests/abi-render.test.tsx +178 -0
  628. package/templates/nova/tests/abi-shop-links.test.tsx +141 -0
  629. package/templates/nova/tests/abi-storefront-basics.test.tsx +233 -0
  630. package/templates/nova/tests/abi-variant-url.test.tsx +73 -0
  631. package/templates/nova/tests/add-refused-no-success.test.tsx +193 -0
  632. package/templates/nova/tests/architecture.test.ts +63 -0
  633. package/templates/nova/tests/architecture.ts +160 -0
  634. package/templates/nova/tests/audit-guards.test.ts +31 -0
  635. package/templates/nova/tests/bundle-split.test.ts +29 -0
  636. package/templates/nova/tests/cart-limit-message.test.tsx +212 -0
  637. package/templates/nova/tests/cart-link-message.test.tsx +98 -0
  638. package/templates/nova/tests/cases/components/Overview.cases.tsx +8 -0
  639. package/templates/nova/tests/cases/components/base/Accordion.cases.tsx +40 -0
  640. package/templates/nova/tests/cases/components/base/Badge.cases.tsx +30 -0
  641. package/templates/nova/tests/cases/components/base/Button.cases.tsx +73 -0
  642. package/templates/nova/tests/cases/components/base/Checkbox.cases.tsx +27 -0
  643. package/templates/nova/tests/cases/components/base/Chip.cases.tsx +37 -0
  644. package/templates/nova/tests/cases/components/base/Divider.cases.tsx +26 -0
  645. package/templates/nova/tests/cases/components/base/IconButton.cases.tsx +28 -0
  646. package/templates/nova/tests/cases/components/base/Input.cases.tsx +28 -0
  647. package/templates/nova/tests/cases/components/base/Link.cases.tsx +27 -0
  648. package/templates/nova/tests/cases/components/base/Radio.cases.tsx +27 -0
  649. package/templates/nova/tests/cases/components/base/Select.cases.tsx +26 -0
  650. package/templates/nova/tests/cases/components/base/Skeleton.cases.tsx +29 -0
  651. package/templates/nova/tests/cases/components/base/Spinner.cases.tsx +16 -0
  652. package/templates/nova/tests/cases/components/base/Switch.cases.tsx +26 -0
  653. package/templates/nova/tests/cases/components/base/Tabs.cases.tsx +39 -0
  654. package/templates/nova/tests/cases/components/base/Textarea.cases.tsx +17 -0
  655. package/templates/nova/tests/cases/components/commerce/AddToCart.cases.tsx +36 -0
  656. package/templates/nova/tests/cases/components/commerce/CartLine.cases.tsx +20 -0
  657. package/templates/nova/tests/cases/components/commerce/CartSummary.cases.tsx +16 -0
  658. package/templates/nova/tests/cases/components/commerce/CollectionCard.cases.tsx +19 -0
  659. package/templates/nova/tests/cases/components/commerce/ComparisonTable.cases.tsx +19 -0
  660. package/templates/nova/tests/cases/components/commerce/Hotspot.cases.tsx +25 -0
  661. package/templates/nova/tests/cases/components/commerce/LookItem.cases.tsx +19 -0
  662. package/templates/nova/tests/cases/components/commerce/PaymentMark.cases.tsx +16 -0
  663. package/templates/nova/tests/cases/components/commerce/Price.cases.tsx +27 -0
  664. package/templates/nova/tests/cases/components/commerce/ProductCard.cases.tsx +67 -0
  665. package/templates/nova/tests/cases/components/commerce/ProductGallery.cases.tsx +39 -0
  666. package/templates/nova/tests/cases/components/commerce/QuantitySelector.cases.tsx +31 -0
  667. package/templates/nova/tests/cases/components/commerce/Reviews.cases.tsx +35 -0
  668. package/templates/nova/tests/cases/components/commerce/Stars.cases.tsx +16 -0
  669. package/templates/nova/tests/cases/components/commerce/VariantPicker.cases.tsx +38 -0
  670. package/templates/nova/tests/cases/components/commerce/blocks/Badge.cases.tsx +17 -0
  671. package/templates/nova/tests/cases/components/commerce/blocks/PaymentIcons.cases.tsx +17 -0
  672. package/templates/nova/tests/cases/components/commerce/blocks/Swatches.cases.tsx +22 -0
  673. package/templates/nova/tests/cases/components/content/ArticleCard.cases.tsx +17 -0
  674. package/templates/nova/tests/cases/components/content/BeforeAfterSlider.cases.tsx +18 -0
  675. package/templates/nova/tests/cases/components/content/CardRow.cases.tsx +23 -0
  676. package/templates/nova/tests/cases/components/content/CarouselControls.cases.tsx +48 -0
  677. package/templates/nova/tests/cases/components/content/ContentsNav.cases.tsx +17 -0
  678. package/templates/nova/tests/cases/components/content/Countdown.cases.tsx +28 -0
  679. package/templates/nova/tests/cases/components/content/HeroCopy.cases.tsx +42 -0
  680. package/templates/nova/tests/cases/components/content/HeroFrame.cases.tsx +22 -0
  681. package/templates/nova/tests/cases/components/content/HeroMedia.cases.tsx +18 -0
  682. package/templates/nova/tests/cases/components/content/LongForm.cases.tsx +17 -0
  683. package/templates/nova/tests/cases/components/content/MediaFrame.cases.tsx +32 -0
  684. package/templates/nova/tests/cases/components/content/RichText.cases.tsx +19 -0
  685. package/templates/nova/tests/cases/components/content/SectionHead.cases.tsx +19 -0
  686. package/templates/nova/tests/cases/components/content/ShareButtons.cases.tsx +24 -0
  687. package/templates/nova/tests/cases/components/content/StoreCard.cases.tsx +17 -0
  688. package/templates/nova/tests/cases/components/content/TestimonialCard.cases.tsx +19 -0
  689. package/templates/nova/tests/cases/components/content/UgcTile.cases.tsx +18 -0
  690. package/templates/nova/tests/cases/components/content/blocks/Button.cases.tsx +20 -0
  691. package/templates/nova/tests/cases/components/content/blocks/Column.cases.tsx +17 -0
  692. package/templates/nova/tests/cases/components/content/blocks/Heading.cases.tsx +18 -0
  693. package/templates/nova/tests/cases/components/content/blocks/Icon.cases.tsx +17 -0
  694. package/templates/nova/tests/cases/components/content/blocks/Image.cases.tsx +22 -0
  695. package/templates/nova/tests/cases/components/content/blocks/Text.cases.tsx +18 -0
  696. package/templates/nova/tests/cases/components/content/blocks/Video.cases.tsx +18 -0
  697. package/templates/nova/tests/cases/components/forms/ContactForm.cases.tsx +29 -0
  698. package/templates/nova/tests/cases/components/forms/FormField.cases.tsx +61 -0
  699. package/templates/nova/tests/cases/components/forms/NewsletterForm.cases.tsx +17 -0
  700. package/templates/nova/tests/cases/components/forms/SearchField.cases.tsx +25 -0
  701. package/templates/nova/tests/cases/components/forms/blocks/Newsletter.cases.tsx +19 -0
  702. package/templates/nova/tests/cases/components/navigation/BrandLogo.cases.tsx +29 -0
  703. package/templates/nova/tests/cases/components/navigation/Breadcrumb.cases.tsx +19 -0
  704. package/templates/nova/tests/cases/components/navigation/LocaleSelector.cases.tsx +32 -0
  705. package/templates/nova/tests/cases/components/navigation/Menu.cases.tsx +27 -0
  706. package/templates/nova/tests/cases/components/navigation/Pagination.cases.tsx +25 -0
  707. package/templates/nova/tests/cases/components/navigation/RecoveryLinks.cases.tsx +16 -0
  708. package/templates/nova/tests/cases/components/navigation/SocialLinks.cases.tsx +18 -0
  709. package/templates/nova/tests/cases/components/navigation/blocks/SocialLinks.cases.tsx +17 -0
  710. package/templates/nova/tests/cases/components/overlays/AccountMenu.cases.tsx +28 -0
  711. package/templates/nova/tests/cases/components/overlays/CartDrawer.cases.tsx +30 -0
  712. package/templates/nova/tests/cases/components/overlays/ConfirmationDialog.cases.tsx +16 -0
  713. package/templates/nova/tests/cases/components/overlays/CookieConsent.cases.tsx +23 -0
  714. package/templates/nova/tests/cases/components/overlays/CookiePreferences.cases.tsx +16 -0
  715. package/templates/nova/tests/cases/components/overlays/Drawer.cases.tsx +52 -0
  716. package/templates/nova/tests/cases/components/overlays/MarketSelector.cases.tsx +82 -0
  717. package/templates/nova/tests/cases/components/overlays/Menu.cases.tsx +27 -0
  718. package/templates/nova/tests/cases/components/overlays/MobileNavDrawer.cases.tsx +32 -0
  719. package/templates/nova/tests/cases/components/overlays/Modal.cases.tsx +71 -0
  720. package/templates/nova/tests/cases/components/overlays/Popover.cases.tsx +50 -0
  721. package/templates/nova/tests/cases/components/overlays/PromoPopup.cases.tsx +19 -0
  722. package/templates/nova/tests/cases/components/overlays/QuickView.cases.tsx +18 -0
  723. package/templates/nova/tests/cases/components/overlays/SearchModal.cases.tsx +60 -0
  724. package/templates/nova/tests/cases/components/overlays/Sheet.cases.tsx +32 -0
  725. package/templates/nova/tests/cases/components/overlays/SizeGuide.cases.tsx +27 -0
  726. package/templates/nova/tests/cases/components/overlays/Toast.cases.tsx +48 -0
  727. package/templates/nova/tests/cases/components/overlays/Tooltip.cases.tsx +33 -0
  728. package/templates/nova/tests/cases/components/states/Alert.cases.tsx +27 -0
  729. package/templates/nova/tests/cases/components/states/EditorFrame.cases.tsx +36 -0
  730. package/templates/nova/tests/cases/components/states/Empty.cases.tsx +30 -0
  731. package/templates/nova/tests/cases/components/states/Error.cases.tsx +38 -0
  732. package/templates/nova/tests/cases/components/states/Loading.cases.tsx +15 -0
  733. package/templates/nova/tests/cases/components/states/MediaState.cases.tsx +29 -0
  734. package/templates/nova/tests/cases/components/states/SkeletonLayout.cases.tsx +20 -0
  735. package/templates/nova/tests/cases/components/states/Unavailable.cases.tsx +16 -0
  736. package/templates/nova/tests/cases/components/states/media/MediaError.cases.tsx +18 -0
  737. package/templates/nova/tests/cases/components/states/media/MediaPlaceholder.cases.tsx +87 -0
  738. package/templates/nova/tests/cases/sections/Overview.cases.tsx +8 -0
  739. package/templates/nova/tests/cases/sections/commerce/collection-list/CollectionList.cases.tsx +46 -0
  740. package/templates/nova/tests/cases/sections/commerce/collection-list/blocks/CollectionItem.cases.tsx +18 -0
  741. package/templates/nova/tests/cases/sections/commerce/featured-collection/FeaturedCollection.cases.tsx +46 -0
  742. package/templates/nova/tests/cases/sections/commerce/featured-product/FeaturedProduct.cases.tsx +40 -0
  743. package/templates/nova/tests/cases/sections/commerce/main-collection/MainCollection.cases.tsx +81 -0
  744. package/templates/nova/tests/cases/sections/commerce/product-details/ProductDetails.cases.tsx +110 -0
  745. package/templates/nova/tests/cases/sections/commerce/product-details/blocks/AddToCartBlock.cases.tsx +21 -0
  746. package/templates/nova/tests/cases/sections/commerce/product-details/blocks/ProductDescription.cases.tsx +17 -0
  747. package/templates/nova/tests/cases/sections/commerce/product-details/blocks/ProductInventory.cases.tsx +17 -0
  748. package/templates/nova/tests/cases/sections/commerce/product-details/blocks/ProductPrice.cases.tsx +17 -0
  749. package/templates/nova/tests/cases/sections/commerce/product-details/blocks/ProductSku.cases.tsx +17 -0
  750. package/templates/nova/tests/cases/sections/commerce/product-details/blocks/ProductTitle.cases.tsx +17 -0
  751. package/templates/nova/tests/cases/sections/commerce/product-details/blocks/Quantity.cases.tsx +20 -0
  752. package/templates/nova/tests/cases/sections/commerce/product-details/blocks/Tab.cases.tsx +29 -0
  753. package/templates/nova/tests/cases/sections/commerce/product-details/blocks/VariantPickerBlock.cases.tsx +32 -0
  754. package/templates/nova/tests/cases/sections/commerce/product-grid/ProductGrid.cases.tsx +48 -0
  755. package/templates/nova/tests/cases/sections/commerce/product-recommendations/ProductRecommendations.cases.tsx +27 -0
  756. package/templates/nova/tests/cases/sections/commerce/recently-viewed/RecentlyViewed.cases.tsx +36 -0
  757. package/templates/nova/tests/cases/sections/content/article-body/ArticleBody.cases.tsx +32 -0
  758. package/templates/nova/tests/cases/sections/content/before-after/BeforeAfter.cases.tsx +40 -0
  759. package/templates/nova/tests/cases/sections/content/before-after/blocks/Comparison.cases.tsx +17 -0
  760. package/templates/nova/tests/cases/sections/content/blog-posts/BlogPosts.cases.tsx +47 -0
  761. package/templates/nova/tests/cases/sections/content/collection-links/CollectionLinks.cases.tsx +36 -0
  762. package/templates/nova/tests/cases/sections/content/faq/Faq.cases.tsx +38 -0
  763. package/templates/nova/tests/cases/sections/content/faq/blocks/FaqItem.cases.tsx +17 -0
  764. package/templates/nova/tests/cases/sections/content/feature-grid-blocks/FeatureGridBlocks.cases.tsx +31 -0
  765. package/templates/nova/tests/cases/sections/content/feature-highlights/FeatureHighlights.cases.tsx +26 -0
  766. package/templates/nova/tests/cases/sections/content/image-gallery/ImageGallery.cases.tsx +31 -0
  767. package/templates/nova/tests/cases/sections/content/image-with-text/ImageWithText.cases.tsx +35 -0
  768. package/templates/nova/tests/cases/sections/content/multicolumn/Multicolumn.cases.tsx +32 -0
  769. package/templates/nova/tests/cases/sections/content/page-body/PageBody.cases.tsx +31 -0
  770. package/templates/nova/tests/cases/sections/content/rich-text/RichTextSection.cases.tsx +33 -0
  771. package/templates/nova/tests/cases/sections/content/team-founders/TeamFounders.cases.tsx +31 -0
  772. package/templates/nova/tests/cases/sections/content/team-founders/blocks/Person.cases.tsx +18 -0
  773. package/templates/nova/tests/cases/sections/content/timeline/Timeline.cases.tsx +27 -0
  774. package/templates/nova/tests/cases/sections/content/timeline/blocks/Step.cases.tsx +17 -0
  775. package/templates/nova/tests/cases/sections/layout/announcement-bar/AnnouncementBar.cases.tsx +119 -0
  776. package/templates/nova/tests/cases/sections/layout/breadcrumbs/Breadcrumbs.cases.tsx +73 -0
  777. package/templates/nova/tests/cases/sections/layout/divider/Divider.cases.tsx +81 -0
  778. package/templates/nova/tests/cases/sections/layout/footer/Footer.cases.tsx +116 -0
  779. package/templates/nova/tests/cases/sections/layout/footer/blocks/FooterBrand.cases.tsx +27 -0
  780. package/templates/nova/tests/cases/sections/layout/footer/blocks/FooterMenu.cases.tsx +22 -0
  781. package/templates/nova/tests/cases/sections/layout/footer/blocks/FooterText.cases.tsx +20 -0
  782. package/templates/nova/tests/cases/sections/layout/group/Group.cases.tsx +185 -0
  783. package/templates/nova/tests/cases/sections/layout/group/blocks/Accordion.cases.tsx +17 -0
  784. package/templates/nova/tests/cases/sections/layout/group/blocks/JumboText.cases.tsx +17 -0
  785. package/templates/nova/tests/cases/sections/layout/group/blocks/Spacer.cases.tsx +33 -0
  786. package/templates/nova/tests/cases/sections/layout/header/Header.cases.tsx +277 -0
  787. package/templates/nova/tests/cases/sections/marketing/collection-banner/CollectionBanner.cases.tsx +62 -0
  788. package/templates/nova/tests/cases/sections/marketing/hero/Hero.cases.tsx +138 -0
  789. package/templates/nova/tests/cases/sections/marketing/marquee/Marquee.cases.tsx +63 -0
  790. package/templates/nova/tests/cases/sections/marketing/promo-banner/PromoBanner.cases.tsx +87 -0
  791. package/templates/nova/tests/cases/sections/marketing/slideshow/Slideshow.cases.tsx +113 -0
  792. package/templates/nova/tests/cases/sections/marketing/slideshow/blocks/Slide.cases.tsx +26 -0
  793. package/templates/nova/tests/cases/sections/marketing/video-hero/VideoHero.cases.tsx +72 -0
  794. package/templates/nova/tests/cases/sections/trust/comparison-table/ComparisonTableSection.cases.tsx +42 -0
  795. package/templates/nova/tests/cases/sections/trust/comparison-table/blocks/CompareItem.cases.tsx +17 -0
  796. package/templates/nova/tests/cases/sections/trust/logo-list/LogoList.cases.tsx +29 -0
  797. package/templates/nova/tests/cases/sections/trust/logo-list/blocks/Logo.cases.tsx +17 -0
  798. package/templates/nova/tests/cases/sections/trust/product-reviews/ProductReviews.cases.tsx +51 -0
  799. package/templates/nova/tests/cases/sections/trust/shop-the-look/ShopTheLook.cases.tsx +51 -0
  800. package/templates/nova/tests/cases/sections/trust/shop-the-look/blocks/Hotspot.cases.tsx +17 -0
  801. package/templates/nova/tests/cases/sections/trust/testimonials/Testimonials.cases.tsx +61 -0
  802. package/templates/nova/tests/cases/sections/trust/testimonials/blocks/Testimonial.cases.tsx +17 -0
  803. package/templates/nova/tests/cases/sections/trust/trust-badges/TrustBadges.cases.tsx +30 -0
  804. package/templates/nova/tests/cases/sections/trust/ugc-gallery/UgcGallery.cases.tsx +32 -0
  805. package/templates/nova/tests/cases/sections/trust/ugc-gallery/blocks/UgcImage.cases.tsx +17 -0
  806. package/templates/nova/tests/cases/sections/utility/account/Account.cases.tsx +44 -0
  807. package/templates/nova/tests/cases/sections/utility/cart-items/CartItems.cases.tsx +43 -0
  808. package/templates/nova/tests/cases/sections/utility/cart-items/blocks/CartBadge.cases.tsx +16 -0
  809. package/templates/nova/tests/cases/sections/utility/contact-form/ContactFormSection.cases.tsx +52 -0
  810. package/templates/nova/tests/cases/sections/utility/contact-form/blocks/ContactDetail.cases.tsx +17 -0
  811. package/templates/nova/tests/cases/sections/utility/newsletter/NewsletterSection.cases.tsx +109 -0
  812. package/templates/nova/tests/cases/sections/utility/not-found/NotFound.cases.tsx +28 -0
  813. package/templates/nova/tests/cases/sections/utility/policy-page/PolicyPage.cases.tsx +29 -0
  814. package/templates/nova/tests/cases/sections/utility/search-results/SearchResultsSection.cases.tsx +39 -0
  815. package/templates/nova/tests/cases/sections/utility/store-locator/StoreLocator.cases.tsx +40 -0
  816. package/templates/nova/tests/cases/styles/Borders.cases.tsx +10 -0
  817. package/templates/nova/tests/cases/styles/Breakpoints.cases.tsx +10 -0
  818. package/templates/nova/tests/cases/styles/Colors.cases.tsx +10 -0
  819. package/templates/nova/tests/cases/styles/Elevation.cases.tsx +10 -0
  820. package/templates/nova/tests/cases/styles/Icon.cases.tsx +23 -0
  821. package/templates/nova/tests/cases/styles/Layout.cases.tsx +8 -0
  822. package/templates/nova/tests/cases/styles/Motion.cases.tsx +10 -0
  823. package/templates/nova/tests/cases/styles/Overview.cases.tsx +8 -0
  824. package/templates/nova/tests/cases/styles/Radius.cases.tsx +10 -0
  825. package/templates/nova/tests/cases/styles/SharedThemeSettings.cases.tsx +19 -0
  826. package/templates/nova/tests/cases/styles/Spacing.cases.tsx +10 -0
  827. package/templates/nova/tests/cases/styles/Typography.cases.tsx +10 -0
  828. package/templates/nova/tests/cases.test.tsx +38 -0
  829. package/templates/nova/tests/components/PromoPopup.test.ts +21 -0
  830. package/templates/nova/tests/components/RichText.test.tsx +65 -0
  831. package/templates/nova/tests/components/formatDate.test.ts +10 -0
  832. package/templates/nova/tests/contract-locks.test.ts +23 -0
  833. package/templates/nova/tests/contracts.test.ts +153 -0
  834. package/templates/nova/tests/cookie-consent-privacy.test.tsx +128 -0
  835. package/templates/nova/tests/empty-store.test.tsx +81 -0
  836. package/templates/nova/tests/fixtures/commerce.ts +128 -0
  837. package/templates/nova/tests/fixtures/content.ts +58 -0
  838. package/templates/nova/tests/fixtures/index.ts +8 -0
  839. package/templates/nova/tests/fixtures/merch.ts +161 -0
  840. package/templates/nova/tests/fixtures/navigation.ts +125 -0
  841. package/templates/nova/tests/fixtures/promotion.ts +43 -0
  842. package/templates/nova/tests/fixtures/sample-store.json +1674 -0
  843. package/templates/nova/tests/fixtures/shop.ts +88 -0
  844. package/templates/nova/tests/fixtures/stress.ts +21 -0
  845. package/templates/nova/tests/inert.test.tsx +285 -0
  846. package/templates/nova/tests/line-add-friend.test.tsx +108 -0
  847. package/templates/nova/tests/localized-copy.test.ts +330 -0
  848. package/templates/nova/tests/product-template-blocks.test.ts +28 -0
  849. package/templates/nova/tests/rewards-launcher.test.tsx +401 -0
  850. package/templates/nova/tests/schema.test.ts +65 -0
  851. package/templates/nova/tests/schema.typetest.ts +7 -0
  852. package/templates/nova/tests/section-groups.test.ts +189 -0
  853. package/templates/nova/tests/sections/Group.test.tsx +37 -0
  854. package/templates/nova/tests/setup.ts +30 -0
  855. package/templates/nova/tests/starter.test.ts +56 -0
  856. package/templates/nova/tests/storefront-click-path.test.tsx +157 -0
  857. package/templates/nova/tests/styles/button-brand.test.ts +24 -0
  858. package/templates/nova/tests/styles/theme.test.ts +43 -0
  859. package/templates/nova/tests/styles/tokens.test.ts +35 -0
  860. package/templates/nova/tests/support/abi-ctx.ts +136 -0
  861. package/templates/nova/tests/support/block-stories.tsx +13 -0
  862. package/templates/nova/tests/support/case.tsx +102 -0
  863. package/templates/nova/tests/support/contracts.ts +30 -0
  864. package/templates/nova/tests/support/foundation-pages.tsx +260 -0
  865. package/templates/nova/tests/support/ia-pages.tsx +154 -0
  866. package/templates/nova/tests/support/listing-demo.tsx +29 -0
  867. package/templates/nova/tests/support/presets.ts +14 -0
  868. package/templates/nova/tests/support/sections.ts +36 -0
  869. package/templates/nova/tests/support/story-content.ts +321 -0
  870. package/templates/nova/tests/support/story-helpers.tsx +37 -0
  871. package/templates/nova/tests/support/with-nova.tsx +26 -0
  872. package/templates/nova/tests/theme-config.test.ts +82 -0
  873. package/templates/nova/tests/theme-integrity.test.ts +315 -0
  874. package/templates/nova/theme.config.ts +134 -0
  875. package/templates/nova/theme.manifest.json +17495 -0
  876. package/templates/nova/tsconfig.json +19 -0
  877. package/templates/nova/vite.abi-ssr.config.ts +37 -0
  878. package/templates/nova/vite.abi.config.ts +26 -0
  879. package/templates/nova/vitest.config.ts +20 -0
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Manifest source-of-truth extractor. Loaded through Vite SSR by
3
+ * `gen-manifest.mjs` so `import.meta.glob` and the ui/ imports resolve against
4
+ * the SAME files the app registers at runtime.
5
+ *
6
+ * Sections come from two places:
7
+ * - the new UI's Section Registry + block schemas, through
8
+ * `app/platform/manifest-sections.ts` — the same translation the runtime registers,
9
+ * so the manifest cannot describe a unit the app does not ship;
10
+ * - custom `sections/*.tsx` `defineSection({...})` files (what an AI build
11
+ * writes). The folder may be empty or absent. A custom file whose name a
12
+ * ui/ unit already owns is shadowed and listed in `warnings`.
13
+ *
14
+ * Everything here is pure data extraction: no React render, no DOM. The
15
+ * section components are replaced with a no-op before anything reads them.
16
+ */
17
+ import { createHash } from 'node:crypto'
18
+ import { buildCatalog, fieldToAttr, type LegacySettings } from './manifest-sections'
19
+ import { FAMILIES } from '../contracts/index'
20
+ import type { Field, Schema, ThemeDefinition } from '@tanqory/theme-kit'
21
+ import type { SectionDef, AttrSpec } from '@tanqory/theme-kit/runtime'
22
+ import type { SectionGroupDoc } from '@tanqory/theme-kit/contract'
23
+ import {
24
+ CONTRACT_VERSION,
25
+ CURRENT_CONTENT_VERSION,
26
+ groupImpact,
27
+ resolvePage,
28
+ roleOf,
29
+ type GroupedPageDoc,
30
+ } from '@tanqory/theme-kit/contract'
31
+
32
+ type SectionModule = { default?: SectionDef }
33
+ interface TemplateNode {
34
+ type?: string
35
+ id?: string
36
+ settings?: Record<string, unknown>
37
+ blocks?: TemplateNode[]
38
+ }
39
+ type TemplateJson = { sections?: TemplateNode[]; groups?: unknown; contentVersion?: number }
40
+
41
+ // ── the theme being built (Vite resolves a leading '/' against its root) ─────
42
+ const only = <T,>(m: Record<string, unknown>): T => Object.values(m)[0] as T
43
+ const theme = only<{ default: ThemeDefinition }>(import.meta.glob('/theme.config.ts', { eager: true })).default
44
+ const settings = only<{ default: Record<string, unknown> }>(import.meta.glob('/config/settings.json', { eager: true })).default
45
+ const settingsSchema = only<{ default: Record<string, AttrSpec> }>(import.meta.glob('/config/settings.schema.ts', { eager: true })).default
46
+ const pkg = only<{ default: { name?: string; version?: string } }>(import.meta.glob('/package.json', { eager: true })).default
47
+ const legacySettings = (Object.values(import.meta.glob('/.tanqory/legacy-settings.json', { eager: true, import: 'default' }))[0] ?? {}) as LegacySettings
48
+ // Section schema files (src/sections/<Name>/schema.ts). Blocks are NOT
49
+ // discovered from folders: each section declares the block schemas it accepts
50
+ // (defineSection({ blocks })). The glob below only maps a declared schema back
51
+ // to its file, for the entry's source hash.
52
+ const schemaMods = import.meta.glob<Record<string, unknown>>('/src/sections/*/schema.ts', { eager: true })
53
+ const blockFiles = import.meta.glob<Record<string, unknown>>('/src/**/blocks/*.tsx', { eager: true })
54
+ // Source text of every theme file an entry can come from, so each entry carries
55
+ // the hash of its source: studio-api compares it with the file it sees and
56
+ // knows whether the entry is current or stale.
57
+ const themeSources = import.meta.glob<string>(['/src/**/*.ts', '/src/**/*.tsx'], { eager: true, query: '?raw', import: 'default' })
58
+ // Custom sections (may be empty / missing — a glob that matches nothing is {}).
59
+ const customMods = import.meta.glob<SectionModule>('/sections/*.tsx', { eager: true })
60
+ const customSources = import.meta.glob<string>('/sections/*.tsx', { eager: true, query: '?raw', import: 'default' })
61
+ const templateMods = import.meta.glob<TemplateJson>('/templates/*.json', { eager: true })
62
+ const groupMods = import.meta.glob<{ default?: SectionGroupDoc }>('/groups/*.json', { eager: true })
63
+
64
+ const isSchema = (v: unknown, kind: 'section' | 'block'): v is Schema =>
65
+ Boolean(v && typeof v === 'object' && (v as Schema).kind === kind && typeof (v as Schema).name === 'string' && (v as Schema).fields)
66
+
67
+ /** Every block schema the theme's sections declare (deduplicated), with its file. */
68
+ function themeBlocks(): { schema: Schema; source: string }[] {
69
+ const fileOf = new Map<unknown, string>()
70
+ for (const [path, mod] of Object.entries(blockFiles)) for (const v of Object.values(mod)) if (isSchema(v, 'block')) fileOf.set(v, path.replace(/^\//, ''))
71
+ const seen = new Set<Schema>()
72
+ const out: { schema: Schema; source: string }[] = []
73
+ for (const section of theme.sections)
74
+ for (const schema of section.blocks ?? []) {
75
+ if (seen.has(schema)) continue
76
+ seen.add(schema)
77
+ out.push({ schema, source: fileOf.get(schema) ?? '' })
78
+ }
79
+ return out
80
+ }
81
+
82
+ /** section id → the file that declares its schema. */
83
+ function sectionSources(): Record<string, string> {
84
+ const out: Record<string, string> = {}
85
+ for (const [path, mod] of Object.entries(schemaMods)) for (const v of Object.values(mod)) if (isSchema(v, 'section')) out[v.name] = path.replace(/^\//, '')
86
+ return out
87
+ }
88
+
89
+ /** The shared settings as the platform's AttrSpecs, labelled from the contracts. */
90
+ function sharedAttributeSpecs(): Record<string, AttrSpec> {
91
+ const shared = theme.sharedSettings
92
+ if (!shared) return {}
93
+ const rows = Object.values(FAMILIES).flatMap((f) => f.shared ?? [])
94
+ const out: Record<string, AttrSpec> = {}
95
+ for (const [key, f] of Object.entries(shared.fields)) {
96
+ const { type, ...rest } = fieldToAttr(key, f as Field, rows.find((r) => r.name === key))
97
+ out[key] = { type, group: shared.groups?.[key] ?? 'General', ...rest }
98
+ }
99
+ return out
100
+ }
101
+
102
+ function serializeAttr(spec: AttrSpec): Record<string, unknown> {
103
+ const out: Record<string, unknown> = {}
104
+ const entries: Array<[string, unknown]> = Object.entries(spec)
105
+ for (const [k, v] of entries) {
106
+ if (typeof v === 'function') continue
107
+ out[k] = v
108
+ }
109
+ return out
110
+ }
111
+
112
+ /** Presets as data: nested blocks and their settings, nothing executable. */
113
+ function serializePresets(def: SectionDef) {
114
+ return (def.presets ?? []).map((p) => ({
115
+ ...(p.name ? { name: p.name } : {}),
116
+ settings: p.settings ?? {},
117
+ blocks: (p.blocks ?? []).map((b) => ({ type: b.type, settings: b.settings ?? {}, ...(b.blocks ? { blocks: b.blocks } : {}) })),
118
+ }))
119
+ }
120
+
121
+ /** sha256 of a file's bytes, shortened — the same shape studio-api uses for a revision. */
122
+ function sourceHash(text: string): string {
123
+ return createHash('sha256').update(text).digest('hex').slice(0, 32)
124
+ }
125
+
126
+ function serializeSection(def: SectionDef, parentsOf: Map<string, string[]>, source?: { path: string; text: string }) {
127
+ const attributes: Record<string, unknown> = {}
128
+ for (const [key, spec] of Object.entries(def.attributes ?? {})) {
129
+ attributes[key] = serializeAttr(spec as AttrSpec)
130
+ }
131
+ const role = roleOf(def)
132
+ // Placement is derived from the same source the runtime reads: a block's
133
+ // parents are the sections whose `allowedBlocks` list it; a layout unit's
134
+ // area is the slot it declares. Never a second hand-maintained list.
135
+ const placement: Record<string, unknown> = { ...(def.placement ?? {}) }
136
+ if (role === 'block' && !placement.parents) placement.parents = parentsOf.get(def.name) ?? []
137
+ if (role === 'layout' && !placement.areas && def.area) placement.areas = [def.area]
138
+ return {
139
+ name: def.name,
140
+ title: def.title,
141
+ ...(def.description ? { description: def.description } : {}),
142
+ // Provenance: which file this entry was generated from and its hash at
143
+ // generation time. Lets a consumer tell a stale entry from a current one
144
+ // instead of trusting the manifest blindly.
145
+ ...(source ? { source: source.path, sourceHash: sourceHash(source.text) } : {}),
146
+ category: def.category ?? 'uncategorized',
147
+ icon: def.icon ?? null,
148
+ // What kind of unit this is and where it may go — the placement contract.
149
+ // `category` is the inserter's display grouping, not a rule.
150
+ role,
151
+ ...(def.area ? { area: def.area } : {}),
152
+ placement,
153
+ requiresContext: def.requiresContext ?? [],
154
+ // The controls the editor auto-renders and the AI must fill to place this
155
+ // section — the section's public contract.
156
+ attributes,
157
+ // What may nest inside it (empty = a leaf section).
158
+ allowedBlocks: def.allowedBlocks ?? [],
159
+ // Starting compositions, nested blocks included, so an inserter or an AI
160
+ // can seed a block-composed section without reading the source.
161
+ presets: serializePresets(def),
162
+ }
163
+ }
164
+
165
+ function fileStem(path: string): string {
166
+ return path.split('/').pop()!.replace(/\.[^.]+$/, '')
167
+ }
168
+
169
+ export function buildManifest() {
170
+ const catalog = buildCatalog({ theme, blocks: themeBlocks(), sectionSources: sectionSources(), legacySettings })
171
+ const themeNames = new Set(catalog.map((e) => e.def.name))
172
+ const sourceOf = (path: string | undefined) => {
173
+ if (!path) return undefined
174
+ const text = themeSources[`/${path}`]
175
+ if (typeof text !== 'string') throw new Error(`[theme-compiler] ${path} is not in the theme`)
176
+ return { path, text }
177
+ }
178
+
179
+ const custom = Object.entries(customMods)
180
+ .map(([path, m]) => ({ def: m.default, source: { path: path.replace(/^\//, ''), text: customSources[path] ?? '' } }))
181
+ .filter((x): x is { def: SectionDef; source: { path: string; text: string } } => !!x.def && typeof x.def.name === 'string')
182
+ // A custom file with a theme section's name overrides it at runtime (it
183
+ // registers last); the manifest lists the name once, as the theme's.
184
+ const shadowedCustomSections = custom
185
+ .filter((x) => themeNames.has(x.def.name))
186
+ .map((x) => ({ name: x.def.name, source: x.source.path }))
187
+ .sort((a, b) => a.source.localeCompare(b.source))
188
+
189
+ const defsWithSource = [
190
+ ...catalog.map((e) => ({ def: e.def as SectionDef, source: sourceOf(e.source) })),
191
+ ...custom.filter((x) => !themeNames.has(x.def.name)),
192
+ ]
193
+ const defs = defsWithSource.map((x) => x.def)
194
+ const parentsOf = new Map<string, string[]>()
195
+ for (const d of defs) for (const b of d.allowedBlocks ?? []) parentsOf.set(b, [...(parentsOf.get(b) ?? []), d.name].sort())
196
+ const sections = defsWithSource.map((x) => serializeSection(x.def, parentsOf, x.source)).sort((a, b) => a.name.localeCompare(b.name))
197
+
198
+ // The shared props, as data. A consumer that reads a custom section's .tsx
199
+ // itself (studio-api, for a file newer than this manifest) applies these the
200
+ // way `withShared` does: shared first, the section's own keys win, `omit`
201
+ // and the content slots left out.
202
+ const sharedAttributes: Record<string, unknown> = {}
203
+ for (const [key, spec] of Object.entries(sharedAttributeSpecs())) sharedAttributes[key] = serializeAttr(spec)
204
+
205
+ // Shared header/footer groups, and which templates each one reaches.
206
+ const groupDocs: Record<string, SectionGroupDoc> = {}
207
+ for (const [path, mod] of Object.entries(groupMods)) if (mod.default) groupDocs[fileStem(path)] = mod.default
208
+ const templateDocs: Record<string, GroupedPageDoc> = {}
209
+ for (const [path, json] of Object.entries(templateMods)) templateDocs[fileStem(path)] = json as GroupedPageDoc
210
+ const groups = Object.entries(groupDocs)
211
+ .map(([name, doc]) => ({
212
+ name,
213
+ type: doc.type,
214
+ sectionTypes: [...new Set(doc.sections.map((s) => s.type))].sort(),
215
+ sectionIds: doc.sections.map((s) => s.id).filter(Boolean),
216
+ // Every template that binds this group — the scope of an edit to it.
217
+ usedBy: groupImpact(name, templateDocs),
218
+ }))
219
+ .sort((a, b) => a.name.localeCompare(b.name))
220
+
221
+ const templates = Object.entries(templateMods)
222
+ .map(([path, json]) => {
223
+ const resolved = resolvePage(json as GroupedPageDoc, groupDocs)
224
+ const secs = Array.isArray(json.sections) ? json.sections : []
225
+ const sectionTypes = [...new Set(secs.map((s) => s.type).filter(Boolean))] as string[]
226
+ const slot = (k: 'header' | 'footer') => {
227
+ const r = resolved.slots[k]
228
+ return r.mode === 'ref' ? { mode: 'ref' as const, group: r.group } : { mode: r.mode }
229
+ }
230
+ return {
231
+ name: fileStem(path),
232
+ contentVersion: json.contentVersion ?? 1,
233
+ sectionCount: secs.length,
234
+ sectionTypes: sectionTypes.sort(),
235
+ // How the page fills its shared slots: `ref` (shared group), `override`
236
+ // (its own copy, on purpose), `inline` (legacy copy), `none`.
237
+ groups: { header: slot('header'), footer: slot('footer') },
238
+ }
239
+ })
240
+ .sort((a, b) => a.name.localeCompare(b.name))
241
+
242
+ // A template that binds a group this theme does not ship renders an empty
243
+ // header — as loud a failure as a dangling section ref.
244
+ const danglingGroupRefs = Object.entries(templateDocs)
245
+ .flatMap(([name, doc]) => resolvePage(doc, groupDocs).missingGroups.map((group) => ({ template: name, group })))
246
+
247
+ // The page layout lives in the theme's definition now, not in layouts/*.tsx files.
248
+ const layouts: string[] = []
249
+
250
+ // Every section type a template references must resolve to a known section —
251
+ // a mismatch means a template points at a section the theme no longer ships.
252
+ const known = new Set(sections.map((s) => s.name))
253
+ const danglingTemplateRefs = templates
254
+ .flatMap((t) => t.sectionTypes.map((type) => ({ template: t.name, type })))
255
+ .filter((ref) => !known.has(ref.type))
256
+
257
+ // Two more classes of template drift:
258
+ //
259
+ // unknownTemplateSettings — a template sets a key the section does not
260
+ // declare. The value is dropped on render and the editor never shows a
261
+ // control for it, so the template appears to configure something it does
262
+ // not.
263
+ // disallowedTemplateBlocks — a template nests a block the parent section's
264
+ // `allowedBlocks` does not permit. The editor would refuse the same
265
+ // composition the theme ships.
266
+ const byName = new Map(sections.map((s) => [s.name, s]))
267
+ const unknownTemplateSettings: Array<{ template: string; type: string; setting: string }> = []
268
+ const disallowedTemplateBlocks: Array<{ template: string; parent: string; type: string }> = []
269
+
270
+ function validateNodes(template: string, nodes: TemplateNode[] | undefined, parent?: string): void {
271
+ for (const node of nodes ?? []) {
272
+ if (!node?.type) continue
273
+ const def = byName.get(node.type)
274
+ if (parent) {
275
+ const parentDef = byName.get(parent)
276
+ if (parentDef && !parentDef.allowedBlocks.includes(node.type)) {
277
+ disallowedTemplateBlocks.push({ template, parent, type: node.type })
278
+ }
279
+ }
280
+ if (def) {
281
+ for (const key of Object.keys(node.settings ?? {})) {
282
+ if (!(key in def.attributes)) {
283
+ unknownTemplateSettings.push({ template, type: node.type, setting: key })
284
+ }
285
+ }
286
+ }
287
+ validateNodes(template, node.blocks, node.type)
288
+ }
289
+ }
290
+
291
+ for (const [path, json] of Object.entries(templateMods)) {
292
+ validateNodes(fileStem(path), Array.isArray(json.sections) ? json.sections : undefined)
293
+ }
294
+
295
+ // Group sections are content too — a group that sets a setting its section
296
+ // does not declare is as broken as a template that does.
297
+ for (const [name, doc] of Object.entries(groupDocs)) {
298
+ validateNodes(`groups/${name}`, doc.sections as TemplateNode[])
299
+ }
300
+
301
+ const categories: Record<string, number> = {}
302
+ for (const s of sections) categories[s.category] = (categories[s.category] ?? 0) + 1
303
+ const roles: Record<string, number> = {}
304
+ for (const s of sections) roles[s.role] = (roles[s.role] ?? 0) + 1
305
+
306
+ // Theme settings, as a typed+grouped schema (settings.schema.ts) plus the
307
+ // current values (settings.json). Serialize each spec the same way section
308
+ // attributes are serialized (drop functions), and group for the editor panel.
309
+ const schemaKeys = Object.keys(settingsSchema)
310
+ const settingGroups: Record<string, Array<{ key: string } & Record<string, unknown>>> = {}
311
+ for (const [key, spec] of Object.entries(settingsSchema)) {
312
+ const s = serializeAttr(spec as AttrSpec)
313
+ const group = (s.group as string) ?? 'General'
314
+ ;(settingGroups[group] ??= []).push({ key, ...s })
315
+ }
316
+
317
+ const valueKeys = Object.keys(settings as Record<string, unknown>)
318
+ // A key with a value but no schema entry is invisible to the editor; a key in
319
+ // the schema with no value falls back to its declared default.
320
+ const settingsUndeclared = valueKeys.filter((k) => !schemaKeys.includes(k))
321
+ // A hidden legacy setting (config/legacy-settings.json) has no value on purpose: it only keeps saved values valid.
322
+ const isLegacy = (k: string) => (settingsSchema[k] as { visible_if?: string } | undefined)?.visible_if === '{{ false }}'
323
+ const settingsMissingValue = schemaKeys.filter((k) => !valueKeys.includes(k) && !isLegacy(k))
324
+
325
+ return {
326
+ $schema: 'https://tanqory.com/schemas/theme-manifest/v1',
327
+ theme: {
328
+ name: pkg.name ?? theme.name,
329
+ version: (pkg as { version?: string }).version ?? '0.0.0',
330
+ },
331
+ // Provenance so a reader knows this is generated, not hand-authored.
332
+ generatedFrom: [
333
+ 'theme.config.ts',
334
+ 'src/sections/*/schema.ts',
335
+ ...(custom.length ? ['sections/*.tsx'] : []),
336
+ 'templates/*.json',
337
+ 'groups/*.json',
338
+ 'config/settings.schema.ts',
339
+ 'config/settings.json',
340
+ ],
341
+ // Which contract this manifest speaks — a consumer compares before trusting it.
342
+ contract: { version: CONTRACT_VERSION, contentVersion: CURRENT_CONTENT_VERSION },
343
+ // `sections` below is the platform catalog: every unit the editor knows,
344
+ // told apart by `role`. In theme terms:
345
+ // Section = what a merchant places — role 'section', plus the header /
346
+ // footer zone sections the platform marks role 'layout'
347
+ // Block = role 'block': only ever nested inside a section
348
+ // Layout = the page shell (theme.config Layout) — never in this list
349
+ stats: {
350
+ units: sections.length,
351
+ sections: (roles.section ?? 0) + (roles.layout ?? 0),
352
+ headerFooterSections: roles.layout ?? 0,
353
+ blocks: roles.block ?? 0,
354
+ blockSchemas: themeBlocks().length,
355
+ templates: templates.length,
356
+ groups: groups.length,
357
+ layouts: layouts.length,
358
+ settings: schemaKeys.length,
359
+ categories,
360
+ roles,
361
+ },
362
+ sections,
363
+ sharedAttributes,
364
+ contentSlots: [...(theme.sharedSettings?.contentSlots ?? [])],
365
+ templates,
366
+ groups,
367
+ layouts,
368
+ // Typed, grouped theme-settings schema (settings.schema.ts) — what the
369
+ // studio's Theme panel renders and the AI generator reads. Groups keep the
370
+ // order settings.schema.ts declares them in.
371
+ settingsSchema: Object.entries(settingGroups).map(([group, items]) => ({ group, items })),
372
+ // Current values (settings.json).
373
+ settings: settings as Record<string, unknown>,
374
+ // Non-fatal integrity signals for the reader / the conformance test.
375
+ warnings: {
376
+ danglingTemplateRefs,
377
+ danglingGroupRefs,
378
+ unknownTemplateSettings,
379
+ disallowedTemplateBlocks,
380
+ settingsUndeclared,
381
+ settingsMissingValue,
382
+ // Custom sections/*.tsx files whose name a ui/ unit owns — not in the catalog.
383
+ shadowedCustomSections,
384
+ },
385
+ }
386
+ }
@@ -0,0 +1,247 @@
1
+ /**
2
+ * A theme's sections and blocks, expressed as the platform's `SectionDef`
3
+ * shape — what `theme.manifest.json` publishes and what the editor, studio-api's
4
+ * save validator and the AI read.
5
+ *
6
+ * The theme's schemas are the source of truth for ids, controls, defaults and
7
+ * option values; this only TRANSLATES them:
8
+ *
9
+ * field.control → AttrSpec.type (the two vocabularies line up 1:1)
10
+ * field.default → AttrSpec.default ('' is omitted: absent = empty)
11
+ * contract row label → AttrSpec.label (fallback: the humanised key)
12
+ * field.options → AttrSpec.options [{ value, label }] with the
13
+ * contract's option labels (fallback: humanised id)
14
+ * field.legacy (options) → appended to options, labelled "… (legacy)", so a
15
+ * saved legacy value still validates
16
+ * field.legacyHidden → declared (saved values validate) but never shown:
17
+ * `visible_if: '{{ false }}'`, no default
18
+ * range / number → min / max / step
19
+ *
20
+ * Wording and placement (title, description, icon, requiresContext) come from
21
+ * each section's definition and each block's schema; a block's parents are
22
+ * derived from the sections' block lists.
23
+ */
24
+ import type { AttrSpec, ContentNode, SectionDef } from '@tanqory/theme-kit/runtime'
25
+ import type { Field, Schema, SectionPreset, ThemeDefinition } from '@tanqory/theme-kit'
26
+ import { FAMILIES, contractBlock, contractSection, type ContractRow, type FamilyId } from '../contracts/index'
27
+
28
+ /** Settings stores saved that a section no longer declares (.tanqory/legacy-settings.json). */
29
+ export type LegacySettings = Readonly<Record<string, Readonly<Record<string, AttrSpec>>>>
30
+
31
+ export interface CatalogInput {
32
+ theme: ThemeDefinition
33
+ /** Every block schema the theme implements, with the file that declares it. */
34
+ blocks: readonly { schema: Schema; source: string }[]
35
+ /** The file that declares each section's schema (section id → theme path). */
36
+ sectionSources: Readonly<Record<string, string>>
37
+ legacySettings: LegacySettings
38
+ }
39
+
40
+ // ── attribute conversion ───────────────────────────────────────────────────
41
+
42
+ /** 'buttonLabel' → 'Button label', 'text-first' → 'Text first'. */
43
+ export function humanise(id: string): string {
44
+ const words = id
45
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
46
+ .replace(/[-_]+/g, ' ')
47
+ .trim()
48
+ .toLowerCase()
49
+ return words ? words[0]!.toUpperCase() + words.slice(1) : id
50
+ }
51
+
52
+ /** Shown on a legacy-hidden setting to a reader of the raw schema (the AI). */
53
+ export const LEGACY_HIDDEN_INFO = 'Legacy setting: saved values keep rendering; not offered for new content.'
54
+ /** Always false, however `visible_if` is read — the editor never shows it. */
55
+ export const LEGACY_HIDDEN_VISIBLE_IF = '{{ false }}'
56
+
57
+ function optionLabel(row: ContractRow | undefined, value: string): string {
58
+ if (row) {
59
+ const i = row.options.indexOf(value)
60
+ if (i >= 0 && row.optionLabels[i]) return row.optionLabels[i]!
61
+ const j = row.legacyOptions.indexOf(value)
62
+ if (j >= 0 && row.legacyOptionLabels[j]) return row.legacyOptionLabels[j]!
63
+ }
64
+ return humanise(value)
65
+ }
66
+
67
+ /** 'Background colour' → 'Background colour (legacy)', never twice. */
68
+ const withLegacyMark = (label: string) => (/\blegacy\b/i.test(label) ? label : `${label} (legacy)`)
69
+
70
+ /** One ui/ schema field → the platform's AttrSpec. */
71
+ export function fieldToAttr(key: string, f: Field, row: ContractRow | undefined): AttrSpec {
72
+ const label = row?.label || humanise(key)
73
+ const spec: AttrSpec = { type: f.control, label }
74
+ if (f.legacyHidden) {
75
+ // Declared so a saved value still passes the save validator (an undeclared
76
+ // key is `unknown_setting`, an error); hidden so it is never a control.
77
+ spec.label = withLegacyMark(label)
78
+ spec.info = LEGACY_HIDDEN_INFO
79
+ spec.visible_if = LEGACY_HIDDEN_VISIBLE_IF
80
+ } else if (f.default !== '') {
81
+ spec.default = f.default
82
+ }
83
+ if (f.control === 'select') {
84
+ const opts = f.options.map((value) => ({ value, label: optionLabel(row, value) }))
85
+ // A saved value that is no longer an option still validates; the ui
86
+ // adapter renders it as the option it maps to.
87
+ for (const [value, to] of Object.entries(f.legacy ?? {})) {
88
+ if (f.options.includes(value)) continue
89
+ const fromContract = row?.legacyOptions.includes(value) ? optionLabel(row, value) : optionLabel(row, to)
90
+ opts.push({ value, label: withLegacyMark(fromContract) })
91
+ }
92
+ spec.options = opts
93
+ }
94
+ if (f.control === 'range' || f.control === 'number') {
95
+ spec.min = f.min
96
+ spec.max = f.max
97
+ spec.step = f.step
98
+ }
99
+ return spec
100
+ }
101
+
102
+ function sectionRow(family: FamilyId, section: string, key: string): ContractRow | undefined {
103
+ return contractSection(family, section)?.rows.find((r) => r.name === key) ?? FAMILIES[family]?.shared.find((r) => r.name === key)
104
+ }
105
+
106
+ function blockRow(family: FamilyId, block: string, parents: readonly string[], key: string, legacyNames: readonly string[] = []): ContractRow | undefined {
107
+ for (const name of [block, ...legacyNames])
108
+ for (const parent of [...parents, undefined]) {
109
+ const row = contractBlock(name, parent)?.block.rows.find((r) => r.name === key)
110
+ if (row) return row
111
+ }
112
+ // A section-as-block (12A newsletter has no rows of its own) is worded like the section it was named after.
113
+ for (const name of legacyNames)
114
+ for (const fam of Object.keys(FAMILIES) as FamilyId[]) {
115
+ const row = contractSection(fam, name)?.rows.find((r) => r.name === key)
116
+ if (row) return row
117
+ }
118
+ return FAMILIES[family]?.shared.find((r) => r.name === key)
119
+ }
120
+
121
+ /** Merge another declaration's attributes in: first wins, select options union. */
122
+ function mergeAttrs(into: Record<string, AttrSpec>, extra: Record<string, AttrSpec>): void {
123
+ for (const [key, spec] of Object.entries(extra)) {
124
+ const have = into[key]
125
+ if (!have) {
126
+ into[key] = spec
127
+ continue
128
+ }
129
+ if (have.options && spec.options) {
130
+ const values = new Set(have.options.map((o) => o.value))
131
+ for (const o of spec.options) if (!values.has(o.value)) have.options.push(o)
132
+ }
133
+ }
134
+ }
135
+
136
+ function presetsOf(presets: readonly SectionPreset[]): SectionDef['presets'] {
137
+ return presets.map((p) => ({
138
+ name: p.label || p.name,
139
+ settings: { ...p.settings },
140
+ blocks: (p.blocks ?? []).map((b): ContentNode => ({ type: b.type, settings: { ...(b.settings ?? {}) } })),
141
+ }))
142
+ }
143
+
144
+ /**
145
+ * Settings the previous theme declared that the new sections do not have.
146
+ * Stores saved them, and the save validator rejects an undeclared key, so each
147
+ * stays declared — hidden, with no default — and the section ignores it.
148
+ * Generated once from the last manifest before the switch; append-only.
149
+ */
150
+ function legacyAttrs(legacy: LegacySettings, section: string): Record<string, AttrSpec> {
151
+ const out: Record<string, AttrSpec> = {}
152
+ for (const [key, old] of Object.entries(legacy[section] ?? {})) {
153
+ out[key] = { ...old, label: withLegacyMark(humanise(key)), info: LEGACY_HIDDEN_INFO, visible_if: LEGACY_HIDDEN_VISIBLE_IF }
154
+ }
155
+ return out
156
+ }
157
+
158
+ // ── the defs ───────────────────────────────────────────────────────────────
159
+
160
+ export interface CatalogEntry {
161
+ def: Omit<SectionDef, 'component'>
162
+ /** Theme path of the file that declares it (the manifest stamps its hash). */
163
+ source?: string
164
+ }
165
+
166
+ /**
167
+ * One entry per theme section, then one per block type not already a section
168
+ * (a type that is both — newsletter — carries both schemas' fields).
169
+ */
170
+ export function buildCatalog(input: CatalogInput): CatalogEntry[] {
171
+ const { theme, legacySettings } = input
172
+ // Block schemas grouped by persisted name; the largest declaration is primary.
173
+ const blocks = new Map<string, { schema: Schema; source: string }[]>()
174
+ for (const b of input.blocks) blocks.set(b.schema.name, [...(blocks.get(b.schema.name) ?? []), b])
175
+ for (const [name, list] of blocks) blocks.set(name, [...list].sort((a, b) => Object.keys(b.schema.fields).length - Object.keys(a.schema.fields).length))
176
+
177
+ // A section may host a legacy block: saved pages still hold it, and the save
178
+ // validator would reject the nesting if allowedBlocks left it out.
179
+ const allowedOf = (s: ThemeDefinition['sections'][number]) => [...new Set([...(s.schema.blocks ?? []), ...(s.schema.legacyBlocks ?? [])])]
180
+ const parentsOf = new Map<string, string[]>()
181
+ for (const s of theme.sections) for (const b of allowedOf(s)) parentsOf.set(b, [...(parentsOf.get(b) ?? []), s.id].sort())
182
+
183
+ const blockAttrs = (name: string): Record<string, AttrSpec> => {
184
+ const attrs: Record<string, AttrSpec> = {}
185
+ const parents = parentsOf.get(name) ?? []
186
+ for (const { schema } of blocks.get(name) ?? []) {
187
+ const own: Record<string, AttrSpec> = {}
188
+ for (const [key, f] of Object.entries(schema.fields)) own[key] = fieldToAttr(key, f as Field, blockRow(schema.family as FamilyId, name, parents, key, schema.legacyNames))
189
+ mergeAttrs(attrs, own)
190
+ }
191
+ return attrs
192
+ }
193
+
194
+ const out: CatalogEntry[] = []
195
+ for (const s of theme.sections) {
196
+ const family = s.schema.family as FamilyId
197
+ const attributes: Record<string, AttrSpec> = {}
198
+ for (const [key, f] of Object.entries(s.schema.fields)) attributes[key] = fieldToAttr(key, f as Field, sectionRow(family, s.id, key))
199
+ if (blocks.has(s.id)) mergeAttrs(attributes, blockAttrs(s.id))
200
+ // A block renamed away from this section's name: saved content still nests
201
+ // it under the old name, which now resolves to this entry — keep its fields.
202
+ for (const [name, list] of blocks) if (list.some((b) => b.schema.legacyNames?.includes(s.id))) mergeAttrs(attributes, blockAttrs(name))
203
+ mergeAttrs(attributes, legacyAttrs(legacySettings, s.id))
204
+ const chrome = s.zone === 'header' || s.zone === 'footer'
205
+ out.push({
206
+ source: input.sectionSources[s.id],
207
+ def: {
208
+ name: s.id,
209
+ title: s.title ?? contractSection(family, s.id)?.label ?? humanise(s.id),
210
+ description: s.description ?? '',
211
+ category: s.category,
212
+ ...(s.icon ? { icon: s.icon } : {}),
213
+ role: chrome ? 'layout' : 'section',
214
+ ...(chrome ? { area: s.zone as 'header' | 'footer' } : {}),
215
+ placement: chrome ? { areas: [s.zone as 'header' | 'footer'] } : {},
216
+ requiresContext: (s.requiresContext ?? []) as SectionDef['requiresContext'],
217
+ attributes,
218
+ allowedBlocks: allowedOf(s),
219
+ presets: presetsOf(s.presets ?? []),
220
+ },
221
+ })
222
+ }
223
+
224
+ const sectionIds = new Set(theme.sections.map((s) => s.id))
225
+ for (const [name, list] of blocks) {
226
+ if (sectionIds.has(name)) continue
227
+ const primary = list[0]!.schema
228
+ const contract = [name, ...(primary.legacyNames ?? [])].map((n) => contractBlock(n, (parentsOf.get(name) ?? [])[0]) ?? contractBlock(n)).find(Boolean)
229
+ out.push({
230
+ source: list[0]!.source,
231
+ def: {
232
+ name,
233
+ title: primary.title ?? contract?.block.label ?? humanise(name),
234
+ description: primary.description ?? '',
235
+ category: 'block',
236
+ ...(primary.icon ? { icon: primary.icon } : {}),
237
+ role: 'block',
238
+ placement: { parents: parentsOf.get(name) ?? [] },
239
+ requiresContext: (primary.requiresContext ?? []) as SectionDef['requiresContext'],
240
+ attributes: blockAttrs(name),
241
+ allowedBlocks: [],
242
+ presets: [],
243
+ },
244
+ })
245
+ }
246
+ return out
247
+ }