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,125 @@
1
+ /**
2
+ * A theme's approved contracts — the frozen persisted ids (sections, blocks,
3
+ * settings, options) with their editor labels and presets. Kept by the theme
4
+ * as locks in .tanqory/contracts/, written by `tanqory-theme contracts`
5
+ * (./extract.mjs) with their hashes in LOCK.json; never hand-edited.
6
+ */
7
+ // The theme's frozen contracts (.tanqory/contracts/<theme>-<family>.json), read
8
+ // from the theme being built — Vite resolves a leading '/' against its root.
9
+ // LOCK.json sits beside them; it is not a family and the filter below skips it.
10
+ const LOCKS = import.meta.glob('/.tanqory/contracts/*.json', { eager: true, import: 'default' }) as Record<string, unknown>
11
+
12
+ export type ContractStatus =
13
+ | 'EXISTING'
14
+ | 'CHANGED'
15
+ | 'NEW'
16
+ | 'LEGACY-HIDDEN'
17
+ | 'NEEDS NEW FIELD TYPE'
18
+ | 'NEEDS NEW DATA SOURCE'
19
+ | 'DATA REQUIREMENT'
20
+ | 'PROPOSED'
21
+ | (string & {})
22
+
23
+ export interface ContractRow {
24
+ name: string
25
+ label: string
26
+ control: string
27
+ options: string[]
28
+ optionLabels: string[]
29
+ legacyOptions: string[]
30
+ legacyOptionLabels: string[]
31
+ def: string | number | boolean | null
32
+ min: number | null
33
+ max: number | null
34
+ step: number | null
35
+ resp: string
36
+ dep: string
37
+ desc: string
38
+ status: ContractStatus
39
+ }
40
+
41
+ export interface ContractSection {
42
+ name: string
43
+ label: string
44
+ status: ContractStatus
45
+ rows: ContractRow[]
46
+ }
47
+
48
+ export interface ContractBlock {
49
+ name: string
50
+ label: string
51
+ parents: string[]
52
+ min: number
53
+ recommended: number
54
+ max: number
55
+ status: ContractStatus
56
+ resp: string
57
+ empty: string
58
+ summary: string
59
+ rows: ContractRow[]
60
+ }
61
+
62
+ export interface ContractPreset {
63
+ name: string
64
+ label: string
65
+ section: string
66
+ purpose: string
67
+ settings: Record<string, unknown>
68
+ blocks: { type: string; settings: Record<string, unknown> }[]
69
+ content: string
70
+ }
71
+
72
+ export type Applicability = 'APPLIES' | 'NOT APPLICABLE' | 'OVERRIDDEN BY SECTION-SPECIFIC BEHAVIOR' | (string & {})
73
+
74
+ export interface ContractFamily {
75
+ sections: ContractSection[]
76
+ blocks: ContractBlock[]
77
+ presets: ContractPreset[]
78
+ shared: ContractRow[]
79
+ sharedApplicability: Record<string, string>[]
80
+ globals: ContractRow[]
81
+ ai: Record<string, unknown>[]
82
+ conflicts: string[]
83
+ placeholders?: { id: string; use: string; viewBox: string; aspect: string; tokens: string[]; density: string; a11y: string }[]
84
+ stateFamilies?: string[]
85
+ }
86
+
87
+ /**
88
+ * Every section contract family the theme ships a lock for, by family id (the
89
+ * `<family>` in the file name). Other locks (design tokens, page specs) share
90
+ * the folder and are skipped: a family is one that declares sections.
91
+ */
92
+ export const FAMILIES: Readonly<Record<string, ContractFamily>> = Object.fromEntries(
93
+ Object.entries(LOCKS)
94
+ .filter(([, v]) => Array.isArray((v as Partial<ContractFamily>)?.sections) && Array.isArray((v as Partial<ContractFamily>)?.blocks))
95
+ .map(([k, v]) => [k.match(/-([^-/]+)\.json$/)?.[1] ?? k, v as ContractFamily]),
96
+ )
97
+
98
+ export type FamilyId = keyof typeof FAMILIES
99
+
100
+ export function contractSection(family: FamilyId, name: string): ContractSection | undefined {
101
+ return FAMILIES[family].sections.find((s) => s.name === name)
102
+ }
103
+
104
+ /**
105
+ * Blocks are defined once in the family that owns them, but a block may list
106
+ * parents in other families (heading is a 12D block that group, a 12A
107
+ * section, accepts). Search every family.
108
+ */
109
+ export function contractBlock(name: string, parent?: string): { family: FamilyId; block: ContractBlock } | undefined {
110
+ for (const [family, f] of Object.entries(FAMILIES) as [FamilyId, ContractFamily][]) {
111
+ const block = f.blocks.find((b) => b.name === name && (!parent || b.parents.includes(parent)))
112
+ if (block) return { family, block }
113
+ }
114
+ return undefined
115
+ }
116
+
117
+ export function presetsFor(section: string): ContractPreset[] {
118
+ return Object.values(FAMILIES).flatMap((f) => f.presets.filter((p) => p.section === section))
119
+ }
120
+
121
+ /** The contract row for a setting, falling back to the family's shared row. */
122
+ export function contractRow(family: FamilyId, owner: { kind: 'section' | 'block'; name: string; parent?: string }, key: string): ContractRow | undefined {
123
+ if (owner.kind === 'block') return contractBlock(owner.name, owner.parent)?.block.rows.find((r) => r.name === key)
124
+ return contractSection(family, owner.name)?.rows.find((r) => r.name === key) ?? FAMILIES[family].shared.find((r) => r.name === key)
125
+ }
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Design → Code contract validation.
3
+ *
4
+ * Compares the code's schemas (what components, the adapter and Storybook use)
5
+ * with the approved contracts (generated/*.json). Every approved setting must
6
+ * be represented by the component API or intentionally handled by the adapter.
7
+ *
8
+ * Issue codes — each is a way the two have drifted apart:
9
+ *
10
+ * missing-section an in-scope contract section has no schema
11
+ * missing-block a contract block an in-scope section accepts has no schema
12
+ * missing-prop a contract row has no field
13
+ * extra-prop a field has no contract row and no stated `extension` reason
14
+ * control-mismatch the field's control differs from the contract's
15
+ * unknown-option the field offers an option the contract does not have
16
+ * missing-option the contract offers an option the field does not have
17
+ * invalid-default the field default differs from the contract default
18
+ * invalid-range range min/max/step differ from the contract
19
+ * invalid-preset a preset names an unknown setting, or a value that is not an option
20
+ * invalid-block a preset / parent names a block the section does not accept
21
+ * duplicated-setting two visible fields control the same semantic
22
+ * unsupported-legacy a LEGACY-HIDDEN row / legacy option is not handled by the field
23
+ * shared-applicability a shared setting's APPLIES / NOT APPLICABLE status is not honoured
24
+ */
25
+ import { FAMILIES, contractBlock, presetsFor, type ContractRow, type FamilyId } from './index'
26
+ import type { Field, Schema } from '@tanqory/theme-kit'
27
+
28
+ export type IssueCode =
29
+ | 'missing-section'
30
+ | 'missing-block'
31
+ | 'missing-prop'
32
+ | 'extra-prop'
33
+ | 'control-mismatch'
34
+ | 'unknown-option'
35
+ | 'missing-option'
36
+ | 'invalid-default'
37
+ | 'invalid-range'
38
+ | 'invalid-preset'
39
+ | 'invalid-block'
40
+ | 'duplicated-setting'
41
+ | 'unsupported-legacy'
42
+ | 'shared-applicability'
43
+
44
+ export interface ContractIssue {
45
+ readonly code: IssueCode
46
+ /** `section:header` · `block:heading@group` · `preset:footer-full` */
47
+ readonly where: string
48
+ readonly key?: string
49
+ readonly message: string
50
+ }
51
+
52
+ /**
53
+ * A difference the design package itself flags as open, recorded with the
54
+ * reason it is accepted. The validator reports these as `accepted`, not as
55
+ * failures, so every exception is visible and reviewed.
56
+ */
57
+ export interface AcceptedDeviation {
58
+ readonly code: IssueCode
59
+ readonly where: string
60
+ readonly key?: string
61
+ readonly reason: string
62
+ }
63
+
64
+ export interface ValidationReport {
65
+ readonly issues: readonly ContractIssue[]
66
+ readonly accepted: readonly (ContractIssue & { reason: string })[]
67
+ /** Rows checked, for the report header. */
68
+ readonly checked: { sections: number; blocks: number; rows: number; presets: number }
69
+ }
70
+
71
+ /**
72
+ * Contract controls that the code carries with a different (but equivalent)
73
+ * control. Only the vocabulary gap is mapped here — never a semantic change.
74
+ */
75
+ const CONTROL_EQUIVALENTS: Record<string, readonly string[]> = {
76
+ text_alignment: ['select'],
77
+ }
78
+
79
+ function sameControl(contract: string, code: string): boolean {
80
+ return contract === code || (CONTROL_EQUIVALENTS[contract]?.includes(code) ?? false)
81
+ }
82
+
83
+ function eqDefault(row: ContractRow, f: Field): boolean {
84
+ const d = row.def
85
+ if (f.control === 'boolean') return (d === true || d === 'true') === f.default
86
+ if (f.control === 'range' || f.control === 'number') return d == null ? true : Number(d) === f.default
87
+ if (f.control === 'select') return String(d ?? '') === f.default
88
+ return String(d ?? '') === String(f.default ?? '')
89
+ }
90
+
91
+ function compareRow(where: string, row: ContractRow, f: Field | undefined, out: ContractIssue[]): void {
92
+ const key = row.name
93
+ if (!f) {
94
+ out.push({ code: 'missing-prop', where, key, message: `contract row "${key}" (${row.control}, ${row.status}) has no field` })
95
+ return
96
+ }
97
+ if (!sameControl(row.control, f.control)) {
98
+ out.push({ code: 'control-mismatch', where, key, message: `control is "${f.control}", contract says "${row.control}"` })
99
+ }
100
+ if (row.status === 'LEGACY-HIDDEN' && !f.legacyHidden) {
101
+ out.push({ code: 'unsupported-legacy', where, key, message: 'contract marks this LEGACY-HIDDEN; the field is not legacyHidden' })
102
+ }
103
+ if (row.status !== 'LEGACY-HIDDEN' && f.legacyHidden) {
104
+ out.push({ code: 'unsupported-legacy', where, key, message: `field is legacyHidden but the contract status is ${row.status}` })
105
+ }
106
+ if (f.control === 'select') {
107
+ const opts = f.options as readonly string[]
108
+ for (const o of opts) if (!row.options.includes(o)) out.push({ code: 'unknown-option', where, key, message: `option "${o}" is not in the contract (${row.options.join(' · ')})` })
109
+ for (const o of row.options) if (!opts.includes(o)) out.push({ code: 'missing-option', where, key, message: `contract option "${o}" is missing` })
110
+ for (const o of row.legacyOptions) {
111
+ if (!f.legacy || !(o in f.legacy)) out.push({ code: 'unsupported-legacy', where, key, message: `legacy value "${o}" is not mapped by the field` })
112
+ }
113
+ }
114
+ if (f.control === 'range' || f.control === 'number') {
115
+ if ((row.min != null && row.min !== f.min) || (row.max != null && row.max !== f.max) || (row.step != null && row.step !== f.step)) {
116
+ out.push({ code: 'invalid-range', where, key, message: `range ${f.min}–${f.max}/${f.step}, contract ${row.min}–${row.max}/${row.step}` })
117
+ }
118
+ }
119
+ if (!eqDefault(row, f)) {
120
+ out.push({ code: 'invalid-default', where, key, message: `default is ${JSON.stringify(f.default)}, contract says ${JSON.stringify(row.def)}` })
121
+ }
122
+ }
123
+
124
+ function compareFields(where: string, rows: readonly ContractRow[], schema: Schema, out: ContractIssue[], sharedKeys: ReadonlySet<string> = new Set()): number {
125
+ const seen = new Set<string>(sharedKeys)
126
+ for (const row of rows) {
127
+ // A row whose control is "—" is a NEEDS NEW DATA SOURCE / PROPOSED marker, not a setting.
128
+ if (row.control === '—' || row.control === '') continue
129
+ seen.add(row.name)
130
+ compareRow(where, row, schema.fields[row.name], out)
131
+ }
132
+ for (const [key, f] of Object.entries(schema.fields)) {
133
+ if (!seen.has(key) && !f.extension) out.push({ code: 'extra-prop', where, key, message: 'field has no contract row and no `extension` reason' })
134
+ }
135
+ // Two visible fields that control the same thing make a merchant choose twice.
136
+ const bySemantic = new Map<string, string[]>()
137
+ for (const [key, f] of Object.entries(schema.fields)) {
138
+ if (f.legacyHidden) continue
139
+ const s = f.semantic ?? key
140
+ bySemantic.set(s, [...(bySemantic.get(s) ?? []), key])
141
+ }
142
+ for (const [s, keys] of bySemantic) {
143
+ if (keys.length > 1) out.push({ code: 'duplicated-setting', where, key: keys.join(' + '), message: `visible fields ${keys.join(', ')} all control "${s}"` })
144
+ }
145
+ return seen.size - [...sharedKeys].filter((k) => !rows.some((r) => r.name === k)).length
146
+ }
147
+
148
+ /**
149
+ * The shared Nova 12 vocabulary is defined ONCE, in 12A ("00 · Shared Nova 12
150
+ * vocabulary"); later families list only their applicability. A family with
151
+ * no shared rows of its own reads 12A's.
152
+ */
153
+ function sharedRowsOf(fam: FamilyId): readonly ContractRow[] {
154
+ return FAMILIES[fam].shared.length ? FAMILIES[fam].shared : FAMILIES['12A'].shared
155
+ }
156
+
157
+ /**
158
+ * The block type a section accepts for a contract block name: the name itself,
159
+ * or the accepted block whose schema lists it in `legacyNames` (a block renamed
160
+ * after the contract was locked — the contract keeps the old name).
161
+ */
162
+ function acceptedBlockFor(contractName: string, schema: Schema, blocks: ReadonlyMap<string, Schema>): string | undefined {
163
+ if (schema.blocks?.includes(contractName)) return contractName
164
+ return schema.blocks?.find((b) => (blocks.get(`${b}@${schema.name}`) ?? blocks.get(b))?.legacyNames?.includes(contractName))
165
+ }
166
+
167
+ function checkPreset(preset: ReturnType<typeof presetsFor>[number], schema: Schema, blocks: ReadonlyMap<string, Schema>, out: ContractIssue[]): void {
168
+ const where = `preset:${preset.name}`
169
+ for (const [key, value] of Object.entries(preset.settings)) {
170
+ const f = schema.fields[key]
171
+ if (!f) {
172
+ out.push({ code: 'invalid-preset', where, key, message: `sets "${key}", which ${schema.name} does not have` })
173
+ continue
174
+ }
175
+ if (f.control === 'select' && !(f.options as readonly string[]).includes(String(value))) {
176
+ out.push({ code: 'invalid-preset', where, key, message: `value "${String(value)}" is not an option of ${schema.name}.${key}` })
177
+ }
178
+ if (f.control === 'boolean' && typeof value !== 'boolean') {
179
+ out.push({ code: 'invalid-preset', where, key, message: `value ${JSON.stringify(value)} is not a boolean` })
180
+ }
181
+ if (f.control === 'range' && (typeof value !== 'number' || value < (f as { min: number }).min || value > (f as { max: number }).max)) {
182
+ out.push({ code: 'invalid-preset', where, key, message: `value ${JSON.stringify(value)} is outside the range` })
183
+ }
184
+ }
185
+ for (const b of preset.blocks) {
186
+ const type = acceptedBlockFor(b.type, schema, blocks)
187
+ if (!type) {
188
+ out.push({ code: 'invalid-block', where, key: b.type, message: `block "${b.type}" is not accepted by ${schema.name}` })
189
+ continue
190
+ }
191
+ const bs = blocks.get(`${type}@${schema.name}`) ?? blocks.get(type)
192
+ if (!bs) {
193
+ out.push({ code: 'invalid-block', where, key: b.type, message: `block "${b.type}" has no schema` })
194
+ continue
195
+ }
196
+ for (const [key, value] of Object.entries(b.settings)) {
197
+ const f = bs.fields[key]
198
+ if (!f) out.push({ code: 'invalid-preset', where, key: `${b.type}.${key}`, message: `block setting "${key}" does not exist` })
199
+ else if (f.control === 'select' && !(f.options as readonly string[]).includes(String(value))) {
200
+ out.push({ code: 'invalid-preset', where, key: `${b.type}.${key}`, message: `value "${String(value)}" is not an option` })
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ export interface ValidateOptions {
207
+ /** Design families whose sections must all be implemented. */
208
+ readonly families: readonly FamilyId[]
209
+ readonly sections: readonly Schema[]
210
+ readonly blocks: readonly Schema[]
211
+ /** Theme-setting groups (kind 'global') — compared with a family's globals rows. */
212
+ readonly globals?: readonly Schema[]
213
+ readonly accepted?: readonly AcceptedDeviation[]
214
+ }
215
+
216
+ export function validateContracts(opts: ValidateOptions): ValidationReport {
217
+ const out: ContractIssue[] = []
218
+ const sectionByName = new Map(opts.sections.map((s) => [s.name, s]))
219
+ // A block schema is keyed by name, and by name@parent when one name has two
220
+ // definitions (image in 12D group vs image in 12F ugc-gallery).
221
+ const blockByKey = new Map<string, Schema>()
222
+ for (const b of opts.blocks) {
223
+ blockByKey.set(b.name, blockByKey.get(b.name) ?? b)
224
+ for (const p of b.parents ?? []) blockByKey.set(`${b.name}@${p}`, b)
225
+ }
226
+ let rows = 0
227
+ let presets = 0
228
+ let blockCount = 0
229
+
230
+ for (const fam of opts.families) {
231
+ const family = FAMILIES[fam]
232
+ // Global settings (Nova 13 popup): one global schema per family carries every globals row.
233
+ if (family.globals.length) {
234
+ const where = `global:${fam}`
235
+ const g = opts.globals?.find((s) => s.family === fam)
236
+ if (!g) out.push({ code: 'missing-section', where, message: `${fam} globals have no global schema` })
237
+ else {
238
+ rows += compareFields(where, family.globals, g, out)
239
+ for (const p of family.presets.filter((x) => x.section.startsWith('global:'))) {
240
+ presets++
241
+ checkPreset(p, g, blockByKey, out)
242
+ }
243
+ }
244
+ }
245
+ for (const cs of family.sections) {
246
+ const where = `section:${cs.name}`
247
+ const schema = sectionByName.get(cs.name)
248
+ if (!schema) {
249
+ out.push({ code: 'missing-section', where, message: `${fam} section "${cs.name}" has no schema` })
250
+ continue
251
+ }
252
+ const sharedRows = sharedRowsOf(fam)
253
+ // A shared setting that APPLIES is a legitimate field even without a
254
+ // section row (12B+ carry no per-section shared rows).
255
+ const applies = new Set(family.sharedApplicability.filter((a) => a[cs.name] === 'APPLIES').map((a) => a.prop))
256
+ rows += compareFields(where, cs.rows, schema, out, applies)
257
+
258
+ // Shared settings: APPLIES must be present with the shared vocabulary;
259
+ // NOT APPLICABLE must be absent unless the section's own row declares it.
260
+ for (const app of family.sharedApplicability) {
261
+ const prop = app.prop
262
+ const status = app[cs.name]
263
+ if (!status) continue
264
+ const own = cs.rows.find((r) => r.name === prop)
265
+ const f = schema.fields[prop]
266
+ if (status === 'APPLIES' && !f) {
267
+ out.push({ code: 'shared-applicability', where, key: prop, message: 'shared setting APPLIES but the section has no field for it' })
268
+ }
269
+ if (status === 'APPLIES' && f && !own) {
270
+ const shared = sharedRows.find((r) => r.name === prop)
271
+ if (shared && shared.control && !sameControl(shared.control, f.control)) {
272
+ out.push({ code: 'control-mismatch', where, key: prop, message: `shared "${prop}" is a ${shared.control}; the field is a ${f.control}` })
273
+ }
274
+ }
275
+ if (status === 'APPLIES' && f?.control === 'select') {
276
+ const shared = sharedRows.find((r) => r.name === prop)
277
+ const opts2 = f.options as readonly string[]
278
+ if (shared && shared.options.some((o) => !opts2.includes(o))) {
279
+ out.push({ code: 'shared-applicability', where, key: prop, message: `does not use the shared vocabulary (${shared.options.join(' · ')})` })
280
+ }
281
+ }
282
+ if (status === 'NOT APPLICABLE' && f && !own) {
283
+ out.push({ code: 'shared-applicability', where, key: prop, message: 'shared setting is NOT APPLICABLE here but the section exposes it' })
284
+ }
285
+ if (status.startsWith('OVERRIDDEN') && f && !own) {
286
+ out.push({ code: 'duplicated-setting', where, key: prop, message: `shared "${prop}" is overridden by ${app[`${cs.name}Note`] || 'a section setting'}; exposing both duplicates it` })
287
+ }
288
+ }
289
+
290
+ // Every contract block that names this section as a parent must be accepted and implemented.
291
+ for (const f2 of Object.values(FAMILIES)) {
292
+ for (const cb of f2.blocks) {
293
+ if (!cb.parents.includes(cs.name)) continue
294
+ if (!acceptedBlockFor(cb.name, schema, blockByKey) && !schema.legacyBlocks?.includes(cb.name)) {
295
+ out.push({ code: 'invalid-block', where, key: cb.name, message: `contract block "${cb.name}" lists ${cs.name} as a parent but the section does not accept it` })
296
+ }
297
+ }
298
+ }
299
+ for (const b of schema.blocks ?? []) {
300
+ const own = blockByKey.get(`${b}@${cs.name}`) ?? blockByKey.get(b)
301
+ const cb = contractBlock(b, cs.name) ?? (own?.legacyNames ?? []).map((n) => contractBlock(n, cs.name)).find(Boolean)
302
+ const where2 = `block:${b}@${cs.name}`
303
+ if (!cb) {
304
+ out.push({ code: 'invalid-block', where: where2, message: `section accepts "${b}" but no contract block lists ${cs.name} as a parent` })
305
+ continue
306
+ }
307
+ const bs = blockByKey.get(`${b}@${cs.name}`) ?? blockByKey.get(b)
308
+ if (!bs) {
309
+ out.push({ code: 'missing-block', where: where2, message: `block "${b}" has no schema` })
310
+ continue
311
+ }
312
+ blockCount++
313
+ rows += compareFields(where2, cb.block.rows, bs, out)
314
+ }
315
+
316
+ for (const p of presetsFor(cs.name)) {
317
+ presets++
318
+ checkPreset(p, schema, blockByKey, out)
319
+ }
320
+ }
321
+ }
322
+
323
+ const accepted: (ContractIssue & { reason: string })[] = []
324
+ const issues: ContractIssue[] = []
325
+ for (const i of out) {
326
+ const a = opts.accepted?.find((x) => x.code === i.code && x.where === i.where && (x.key === undefined || x.key === i.key))
327
+ if (a) accepted.push({ ...i, reason: a.reason })
328
+ else issues.push(i)
329
+ }
330
+ // Dedupe (a block reached from two parents reports once per parent — keep that; exact dupes go).
331
+ const uniq = (xs: ContractIssue[]) => xs.filter((x, i) => xs.findIndex((y) => y.code === x.code && y.where === x.where && y.key === x.key && y.message === x.message) === i)
332
+ return {
333
+ issues: uniq(issues),
334
+ accepted: uniq(accepted) as (ContractIssue & { reason: string })[],
335
+ checked: { sections: opts.families.reduce((n, f) => n + FAMILIES[f].sections.length, 0), blocks: blockCount, rows, presets },
336
+ }
337
+ }
338
+
339
+ export function formatReport(r: ValidationReport): string {
340
+ const lines = [
341
+ `Contract validation — ${r.checked.sections} sections · ${r.checked.blocks} block bindings · ${r.checked.rows} rows · ${r.checked.presets} presets`,
342
+ ]
343
+ for (const i of r.issues) lines.push(` ✗ [${i.code}] ${i.where}${i.key ? ` · ${i.key}` : ''} — ${i.message}`)
344
+ for (const a of r.accepted) lines.push(` ~ [${a.code}] ${a.where}${a.key ? ` · ${a.key}` : ''} — accepted: ${a.reason}`)
345
+ lines.push(r.issues.length ? `✗ ${r.issues.length} issue(s)` : `✓ no drift (${r.accepted.length} accepted deviation(s))`)
346
+ return lines.join('\n')
347
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Emit theme.manifest.json — the single machine-readable catalog of this theme,
3
+ * read by the editor inserter, the AI theme generator (ai-api), and the Phase 1
4
+ * dashboard↔surface conformance test.
5
+ *
6
+ * It loads `manifest-entry.ts` through a Vite SSR server rather than plain Node
7
+ * so `import.meta.glob` resolves against the exact `sections/*.tsx` +
8
+ * `templates/*.json` the app registers — the manifest is generated from the
9
+ * same source of truth it describes, so it cannot drift.
10
+ *
11
+ * node scripts/gen-manifest.mjs [--check]
12
+ *
13
+ * --check regenerates in memory and fails (exit 1) if the committed
14
+ * theme.manifest.json is stale — for CI, so a new/edited section without a
15
+ * regenerated manifest is caught in review.
16
+ */
17
+ import { createServer } from 'vite'
18
+ import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs'
19
+ import { fileURLToPath } from 'node:url'
20
+ import { dirname, join } from 'node:path'
21
+
22
+ const here = dirname(fileURLToPath(import.meta.url))
23
+ // The theme being built: the directory the command runs in.
24
+ const root = process.cwd()
25
+ // theme.manifest.json stays at the theme root: studio-api and the editor read
26
+ // it from a theme's files, and the runtime bundle imports it. It is committed,
27
+ // so a clean checkout builds without regenerating it first.
28
+ const outFile = join(root, 'theme.manifest.json')
29
+ const readmeFile = join(root, 'README.md')
30
+ const check = process.argv.includes('--check')
31
+
32
+ const CATALOG_START = '<!-- BEGIN GENERATED CATALOG -->'
33
+ const CATALOG_END = '<!-- END GENERATED CATALOG -->'
34
+
35
+ /** Render the human-readable catalog block spliced into README.md — a view of
36
+ * the same manifest, so the docs can't drift from what the theme ships. */
37
+ function renderCatalog(m) {
38
+ const byCat = {}
39
+ for (const s of m.sections) if (s.role !== 'block') (byCat[s.category] ??= []).push(s.name)
40
+ const blocks = m.sections.filter((s) => s.role === 'block').map((s) => s.name).sort()
41
+ const lines = []
42
+ lines.push(
43
+ `**${m.stats.sections} sections (${m.stats.headerFooterSections} in the header / footer zones) · ${m.stats.blocks} block types · ${m.stats.templates} templates · ${m.stats.settings} settings**`,
44
+ )
45
+ lines.push('')
46
+ lines.push('### Sections by category')
47
+ for (const cat of Object.keys(byCat).sort()) {
48
+ lines.push(`- **${cat}** (${byCat[cat].length}): ${byCat[cat].sort().join(', ')}`)
49
+ }
50
+ lines.push('')
51
+ lines.push(`### Blocks (${blocks.length} types, nested inside sections)`)
52
+ lines.push(`- ${blocks.join(', ')}`)
53
+ lines.push('')
54
+ lines.push('### Shared groups')
55
+ for (const g of m.groups ?? []) {
56
+ lines.push(`- **${g.name}** (${g.type}): ${g.sectionTypes.join(', ')} — used by ${g.usedBy.length} template(s)`)
57
+ }
58
+ lines.push('')
59
+ lines.push('### Templates')
60
+ lines.push('| template | header | footer | sections |')
61
+ lines.push('| --- | --- | --- | --- |')
62
+ const slot = (s) => (s.mode === 'ref' ? `↗ ${s.group}` : s.mode)
63
+ for (const t of m.templates)
64
+ lines.push(`| \`${t.name}\` | ${slot(t.groups.header)} | ${slot(t.groups.footer)} | ${t.sectionTypes.join(', ') || '—'} |`)
65
+ lines.push('')
66
+ lines.push('### Theme settings')
67
+ for (const g of m.settingsSchema) {
68
+ lines.push(`- **${g.group}**: ${g.items.map((i) => i.key).join(', ')}`)
69
+ }
70
+ return lines.join('\n')
71
+ }
72
+
73
+ /** Splice a freshly-rendered catalog between the markers. Returns the new
74
+ * README text, or throws if the markers are missing. */
75
+ function spliceReadme(current, catalog) {
76
+ const a = current.indexOf(CATALOG_START)
77
+ const b = current.indexOf(CATALOG_END)
78
+ if (a === -1 || b === -1 || b < a) {
79
+ throw new Error(`README.md is missing the ${CATALOG_START} / ${CATALOG_END} markers`)
80
+ }
81
+ return current.slice(0, a + CATALOG_START.length) + '\n' + catalog + '\n' + current.slice(b)
82
+ }
83
+
84
+ /** Print every integrity warning the manifest carries. */
85
+ function reportWarnings(w, log) {
86
+ for (const d of w.danglingTemplateRefs) log(`⚠ template ${d.template} → unknown section ${d.type}`)
87
+ for (const d of w.danglingGroupRefs ?? []) log(`⚠ template ${d.template} → unknown group ${d.group} (groups/${d.group}.json)`)
88
+ for (const u of w.unknownTemplateSettings ?? [])
89
+ log(`⚠ template ${u.template} → ${u.type} sets '${u.setting}', which that section does not declare`)
90
+ for (const b of w.disallowedTemplateBlocks ?? [])
91
+ log(`⚠ template ${b.template} → ${b.parent} nests '${b.type}', not in its allowedBlocks`)
92
+ for (const k of w.settingsUndeclared) log(`⚠ settings.json key '${k}' has no schema entry`)
93
+ for (const k of w.settingsMissingValue) log(`⚠ schema key '${k}' has no value in settings.json`)
94
+ }
95
+
96
+ /** Drift that must fail CI (content that cannot work as written). */
97
+ function countDrift(w) {
98
+ return (
99
+ w.danglingTemplateRefs.length +
100
+ (w.danglingGroupRefs ?? []).length +
101
+ (w.unknownTemplateSettings ?? []).length +
102
+ (w.disallowedTemplateBlocks ?? []).length
103
+ )
104
+ }
105
+
106
+ const server = await createServer({
107
+ root,
108
+ logLevel: 'error',
109
+ server: { middlewareMode: true },
110
+ // The section files import CSS/assets; Vite SSR stubs those, so importing a
111
+ // section to read its static definition is safe.
112
+ appType: 'custom',
113
+ })
114
+
115
+ try {
116
+ const mod = await server.ssrLoadModule(join(here, 'manifest-entry.ts'))
117
+ const manifest = mod.buildManifest()
118
+ const json = JSON.stringify(manifest, null, 2) + '\n'
119
+
120
+ const newReadme = spliceReadme(readFileSync(readmeFile, 'utf8'), renderCatalog(manifest))
121
+
122
+ if (check) {
123
+ const curJson = existsSync(outFile) ? readFileSync(outFile, 'utf8') : ''
124
+ const curReadme = readFileSync(readmeFile, 'utf8')
125
+ const staleJson = curJson !== json
126
+ const staleReadme = curReadme !== newReadme
127
+ // Template drift is a CI failure, not a warning: a template that sets a
128
+ // setting the section does not declare, nests a block the parent forbids,
129
+ // or points at a section that no longer exists is broken content shipped
130
+ // as if it worked.
131
+ const drift = countDrift(manifest.warnings)
132
+ if (drift > 0) {
133
+ reportWarnings(manifest.warnings, console.error)
134
+ console.error(`✗ ${drift} template/settings drift problem(s). Fix the template or declare the setting.`)
135
+ process.exitCode = 1
136
+ }
137
+ if (staleJson || staleReadme) {
138
+ const what = [staleJson && 'theme.manifest.json', staleReadme && 'README.md catalog']
139
+ .filter(Boolean)
140
+ .join(' + ')
141
+ console.error(`✗ ${what} stale. Run \`tanqory-theme manifest\` and commit the result.`)
142
+ process.exitCode = 1
143
+ } else {
144
+ console.log('✓ theme.manifest.json + README catalog are up to date.')
145
+ }
146
+ } else {
147
+ mkdirSync(dirname(outFile), { recursive: true })
148
+ writeFileSync(outFile, json)
149
+ writeFileSync(readmeFile, newReadme)
150
+ const { sections, blocks, templates } = manifest.stats
151
+ console.log(`✓ theme.manifest.json + README catalog — ${sections} sections, ${blocks} block types, ${templates} templates`)
152
+ reportWarnings(manifest.warnings, console.warn)
153
+ }
154
+ } finally {
155
+ await server.close()
156
+ }