create-tanqory-theme 0.0.0-stage → 0.2.2

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,169 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Layout sanity — the check that was missing every time a screenshot looked
4
+ * wrong while every gate passed.
5
+ *
6
+ * Nothing in this repo looked at PROPORTION. A footer cell stretched to 404px
7
+ * around 120px of payment marks, a newsletter squeezed into a 163px column with
8
+ * its email field clipped and its heading broken across two lines, a question
9
+ * that opened onto an empty panel — all of it passed typecheck, 209 unit tests,
10
+ * the design-rule checker and a full accessibility audit, because none of those
11
+ * ask whether a block is mostly empty or a line is four words long.
12
+ *
13
+ * Three measurements, at every breakpoint the design names:
14
+ * 1. a laid-out box far taller than what it contains
15
+ * 2. a text column so narrow the copy breaks into very short lines
16
+ * 3. a form field too narrow to show what a person types into it
17
+ *
18
+ * pnpm check:layout # needs the dev server running
19
+ */
20
+ import { chromium } from 'playwright-core'
21
+
22
+ const BASE = process.env.THEME_URL ?? 'http://localhost:4321'
23
+ const WIDTHS = [1440, 1280, 768, 390]
24
+ const ROUTES = ['/', '/products/classic-double-breasted-blazer', '/collections/test-collection', '/cart']
25
+
26
+ /** Space a box holds beyond its content before it reads as a gap. */
27
+ const WASTE_PX = 120
28
+ /** Below this, a line of prose is too short to read comfortably. */
29
+ const MIN_CHARS_PER_LINE = 18
30
+ /** An email field narrower than this clips what has been typed. */
31
+ const MIN_FIELD_PX = 180
32
+
33
+ /** Runs in the page. A real function, not a string — passed as a string,
34
+ * Playwright read this arrow function with a destructured parameter as a
35
+ * statement and returned `undefined` for every page, so the gate reported
36
+ * success on a fixture built to fail three ways. */
37
+ function probe([wastePx, minChars, minField]) {
38
+ const out = []
39
+ /** Anything inside a closed overlay is not on screen; measuring it reports
40
+ * gaps nobody can see. */
41
+ const onScreen = (e) => {
42
+ for (let n = e; n; n = n.parentElement) {
43
+ if (n.hasAttribute?.('inert')) return false
44
+ const cs = getComputedStyle(n)
45
+ if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false
46
+ }
47
+ return true
48
+ }
49
+ const boxes = [...document.querySelectorAll('main *, .site-footer *, .site-header *')]
50
+ for (const e of boxes) {
51
+ const r = e.getBoundingClientRect()
52
+ if (r.height < 80 || r.width < 40) continue
53
+ if (!onScreen(e)) continue
54
+ const cs = getComputedStyle(e)
55
+ if (cs.position === 'absolute' || cs.position === 'fixed') continue
56
+ // A box whose size the author SET is not a gap. A hero is 640px because
57
+ // the merchant chose "large" and its type sits at the bottom of the frame;
58
+ // a thumbnail rail is as tall as the photo beside it. This check is for
59
+ // space a layout leaves by accident, not space reserved on purpose.
60
+ // `getComputedStyle().height` is the USED value and is never 'auto' for a
61
+ // rendered element, so testing it disabled this check entirely — the
62
+ // fixture built to fail stopped failing. Only a declared minimum, a fixed
63
+ // ratio or a background image count as "the author asked for this space".
64
+ if (cs.aspectRatio !== 'auto') continue
65
+ if (cs.minHeight !== '0px' && cs.minHeight !== 'auto') continue
66
+ if (cs.backgroundImage !== 'none') continue
67
+ // Only overlays are excluded. Filtering on `position === 'static'` also
68
+ // dropped every relatively-positioned child — a product card's media is
69
+ // `position: relative`, so the card looked 80% empty.
70
+ const kids = [...e.children].filter((c) => {
71
+ const p = getComputedStyle(c).position
72
+ return p !== 'absolute' && p !== 'fixed'
73
+ })
74
+ if (!kids.length) continue
75
+ // The SPAN the children occupy, not the sum of their heights. Summing is
76
+ // only right for a vertical stack; in a grid or a row the children sit side
77
+ // by side, and adding their heights made a perfectly packed row of columns
78
+ // look like a mostly empty box.
79
+ const rects = kids.map((c) => c.getBoundingClientRect()).filter((b) => b.height > 0)
80
+ if (!rects.length) continue
81
+ const top = Math.min(...rects.map((b) => b.top))
82
+ const bottom = Math.max(...rects.map((b) => b.bottom))
83
+ // Padding is deliberate space, not a gap in the layout.
84
+ const chrome = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom)
85
+ const content = bottom - top + chrome
86
+ if (content > 0 && r.height - content > wastePx) {
87
+ out.push({ kind: 'empty-space', el: (e.className || e.tagName).toString().split(' ')[0],
88
+ detail: `${Math.round(r.height - content)}px of a ${Math.round(r.height)}px box holds nothing` })
89
+ }
90
+ }
91
+ for (const e of document.querySelectorAll('p, .lede, .newsletter__note')) {
92
+ if (!onScreen(e)) continue
93
+ const r = e.getBoundingClientRect()
94
+ const text = (e.textContent || '').trim()
95
+ if (r.height < 40 || text.length < 60) continue
96
+ const lh = parseFloat(getComputedStyle(e).lineHeight) || 20
97
+ const lines = Math.round(r.height / lh)
98
+ if (lines >= 3 && text.length / lines < minChars) {
99
+ out.push({ kind: 'cramped-text', el: (e.className || e.tagName).toString().split(' ')[0],
100
+ detail: `${Math.round(text.length / lines)} characters per line over ${lines} lines` })
101
+ }
102
+ }
103
+ for (const e of document.querySelectorAll('input[type=email], input[type=search], input[type=text]')) {
104
+ if (!onScreen(e)) continue
105
+ const r = e.getBoundingClientRect()
106
+ if (r.height > 0 && r.width < minField) {
107
+ out.push({ kind: 'clipped-field', el: (e.className || e.type).toString().split(' ')[0],
108
+ detail: `${Math.round(r.width)}px wide` })
109
+ }
110
+ }
111
+ const seen = new Set()
112
+ return out.filter((o) => {
113
+ const k = o.kind + o.el + o.detail
114
+ if (seen.has(k)) return false
115
+ seen.add(k)
116
+ return true
117
+ })
118
+ }
119
+
120
+
121
+ // `playwright-core` ships no browser binaries, so it drives the Chrome already
122
+ // installed on the machine. Connecting over CDP to a shared Chrome was tried
123
+ // first and silently returned nothing on every page — a gate that reports
124
+ // success because it never looked is worse than no gate, which is why the
125
+ // sanity assertions below exist.
126
+ const browser = await chromium.launch({ channel: 'chrome' })
127
+
128
+ let total = 0
129
+ for (const width of WIDTHS) {
130
+ // A CDP connection hands back the browser's existing context; creating a new
131
+ // one is not supported, so the viewport is set on the page instead.
132
+ const context = await browser.newContext({ viewport: { width, height: 1000 } })
133
+ const page = await context.newPage()
134
+ for (const route of ROUTES) {
135
+ await page.goto(BASE + route, { waitUntil: 'networkidle' })
136
+ await page.waitForTimeout(4500)
137
+ try { await page.click('button:has-text("Accept")', { timeout: 1500 }) } catch { /* none */ }
138
+ // A silent empty result is how a gate becomes decoration: assert the page
139
+ // is actually loaded and sized before believing that it found nothing.
140
+ const sanity = await page.evaluate(() => ({
141
+ url: location.href,
142
+ width: document.documentElement.clientWidth,
143
+ boxes: document.querySelectorAll('main *, .site-footer *, .site-header *').length,
144
+ }))
145
+ if (sanity.boxes === 0) {
146
+ console.error(` ${width}px ${route} — page produced no elements to measure (${sanity.url})`)
147
+ process.exitCode = 1
148
+ continue
149
+ }
150
+ if (Math.abs(sanity.width - width) > 40) {
151
+ console.error(` asked for ${width}px, the page reports ${sanity.width}px — viewport not applied`)
152
+ process.exitCode = 1
153
+ continue
154
+ }
155
+ const found = await page.evaluate(probe, [WASTE_PX, MIN_CHARS_PER_LINE, MIN_FIELD_PX])
156
+ for (const f of found) {
157
+ total++
158
+ console.error(` ${width}px ${route} — [${f.kind}] ${f.el}: ${f.detail}`)
159
+ }
160
+ }
161
+ await context.close()
162
+ }
163
+ await browser.close()
164
+
165
+ if (total) {
166
+ console.error(`\n✗ ${total} layout problem(s)`)
167
+ process.exit(1)
168
+ }
169
+ console.log(`✓ no layout problems across ${ROUTES.length} routes x ${WIDTHS.length} widths`)
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Exactly one top-level heading per template.
4
+ *
5
+ * A live sweep catches this on routable pages, but `templateSuffix` variants
6
+ * (product.bundle, collection.featured, blog.featured) need a product or
7
+ * collection assigned to them and so are never visited in a local run. Three
8
+ * of them shipped with two `h1`s because a decorative rich-text was set to the
9
+ * h1 size above a section that already owns the page heading.
10
+ *
11
+ * The list below is the source of truth for which sections own an `h1`, and is
12
+ * asserted against the code by tests/design-system.test.ts.
13
+ */
14
+ import { readFileSync, readdirSync } from 'node:fs'
15
+ import { join, dirname } from 'node:path'
16
+ import { fileURLToPath } from 'node:url'
17
+
18
+ // The theme: the directory the command runs in.
19
+ const ROOT = process.cwd()
20
+
21
+ /** Sections that always render the page's `h1`. */
22
+ const OWNS_H1 = new Set([
23
+ 'product-details', 'blog-posts', 'page-body',
24
+ 'article-body', 'cart-items', 'account', 'search-results',
25
+ 'not-found', 'policy-page',
26
+ ])
27
+ /** Sections that own it only in some configuration. */
28
+ const CONDITIONAL = {
29
+ // Only the first slide, and only when the slideshow is the page's opener.
30
+ slideshow: () => true,
31
+ hero: () => true,
32
+ // Only on the /collections index route, where it is the whole page.
33
+ 'collection-list': (_s, template) => template === 'list-collections',
34
+ // Only when the merchant picks the h1 size.
35
+ 'rich-text': (s) => (s.settings ?? {}).headingSize === 'h1',
36
+ // Direction B: the banner carries the title only when told to, and is then
37
+ // paired with `main-collection.showHeader: false` (checked below).
38
+ 'collection-banner': (s) => (s.settings ?? {}).pageHeading === true,
39
+ // 07B: the contact page's first section carries "Contact us" when told to.
40
+ 'contact-form': (s) => (s.settings ?? {}).pageHeading === true,
41
+ // The collection page's own header can be switched off for that pairing.
42
+ 'main-collection': (s) => (s.settings ?? {}).showHeader !== false,
43
+ }
44
+
45
+ const problems = []
46
+ for (const file of readdirSync(join(ROOT, 'templates')).filter((f) => f.endsWith('.json'))) {
47
+ const doc = JSON.parse(readFileSync(join(ROOT, 'templates', file), 'utf8'))
48
+ const body = (doc.sections ?? []).filter(
49
+ (s) => s.area !== 'header' && !['header', 'footer', 'announcement-bar'].includes(s.type),
50
+ )
51
+ const owners = body.filter(
52
+ (s) => OWNS_H1.has(s.type) || (CONDITIONAL[s.type]?.(s, file.replace(/\.json$/, '')) ?? false),
53
+ )
54
+ if (owners.length > 1) {
55
+ problems.push(`${file}: ${owners.length} sections claim the page heading — ${owners.map((s) => s.type).join(', ')}`)
56
+ }
57
+ if (owners.length === 0) {
58
+ problems.push(`${file}: no section carries the page heading`)
59
+ }
60
+ }
61
+
62
+ if (problems.length) {
63
+ console.error('✗ page heading problems:')
64
+ for (const p of problems) console.error(` ${p}`)
65
+ process.exit(1)
66
+ }
67
+ console.log('✓ every template has exactly one section carrying the page heading')
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `pnpm check:jsonld` — the BUILT theme must ship schema.org Product/Offer JSON-LD in the SERVER html of a product
4
+ * page (crawlers and AI answer engines do not run JS) and nothing on non-product pages.
5
+ *
6
+ * Runs the real SSR entry (`dist/ssr/entry.mjs`, so build first) against the theme's mock catalog and checks what
7
+ * Google's Product snippet requires: `name`, an absolute `url`, and an `offers` block with a numeric `price`, an
8
+ * ISO-4217 `priceCurrency` and a schema.org `availability` URL. Per-market currency and availability logic is
9
+ * unit-tested in tests/structured-data.test.ts; this guards the wiring (build -> render -> head).
10
+ */
11
+ import { access } from 'node:fs/promises'
12
+ import { join, resolve } from 'node:path'
13
+ import { pathToFileURL } from 'node:url'
14
+
15
+ const root = resolve(process.argv[2] ?? '.')
16
+ const entry = join(root, 'dist', 'ssr', 'entry.mjs')
17
+ try {
18
+ await access(entry)
19
+ } catch {
20
+ console.error('error dist/ssr/entry.mjs is missing: run `pnpm build` first')
21
+ process.exit(1)
22
+ }
23
+
24
+ const mod = await import(pathToFileURL(entry).href)
25
+ const ctx = { mode: 'serve', assets: { base: '', manifest: {} }, content: { templates: {}, groups: {}, settings: {} } }
26
+ const errors = []
27
+ const CURRENCIES = new Set(Intl.supportedValuesOf('currency'))
28
+ const AVAILABILITY = new Set(['InStock', 'OutOfStock', 'PreOrder', 'BackOrder', 'SoldOut', 'LimitedAvailability', 'Discontinued', 'InStoreOnly', 'OnlineOnly'].map((a) => `https://schema.org/${a}`))
29
+ const page = async (path) => {
30
+ const res = await mod.render(new Request(`https://shop.example.com${path}`), ctx)
31
+ return { status: res.status, html: await res.text() }
32
+ }
33
+ const blocks = (html) => [...html.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => m[1])
34
+ /** The Product blocks only: sections add their own types (BreadcrumbList, FAQPage), which are not duplicates. */
35
+ const productBlocks = (html) =>
36
+ blocks(html).filter((b) => {
37
+ try {
38
+ return JSON.parse(b)['@type'] === 'Product'
39
+ } catch {
40
+ return true // unparseable counts, so the check below reports it
41
+ }
42
+ })
43
+
44
+ const product = await page('/products/example-product-1')
45
+ const found = productBlocks(product.html)
46
+ if (product.status !== 200) errors.push(`product page answered ${product.status}`)
47
+ if (found.length !== 1) errors.push(`expected exactly one Product JSON-LD block on the product page, found ${found.length}`)
48
+ else {
49
+ let ld
50
+ try {
51
+ ld = JSON.parse(found[0])
52
+ } catch (e) {
53
+ errors.push(`JSON-LD is not valid JSON: ${e.message}`)
54
+ }
55
+ if (ld) {
56
+ if (ld['@context'] !== 'https://schema.org') errors.push('@context must be https://schema.org')
57
+ if (ld['@type'] !== 'Product') errors.push('@type must be Product')
58
+ if (!ld.name) errors.push('name is required')
59
+ if (!/^https?:\/\//.test(ld.url ?? '')) errors.push('url must be absolute')
60
+ const o = ld.offers
61
+ if (!o) errors.push('offers is required')
62
+ else {
63
+ const price = o['@type'] === 'AggregateOffer' ? o.lowPrice : o.price
64
+ if (!/^\d+(?:\.\d+)?$/.test(String(price ?? ''))) errors.push(`offers price is not numeric: ${price}`)
65
+ if (!CURRENCIES.has(o.priceCurrency)) errors.push(`offers priceCurrency is not ISO-4217: ${o.priceCurrency}`)
66
+ if (o.availability !== undefined && !AVAILABILITY.has(o.availability)) errors.push(`offers availability is not a schema.org URL: ${o.availability}`)
67
+ }
68
+ for (const img of ld.image ?? []) if (!/^https?:\/\//.test(img)) errors.push(`image is not absolute: ${img}`)
69
+ }
70
+ }
71
+ if (/<\/script><script/.test(found[0] ?? '')) errors.push('JSON-LD must not contain a raw script terminator')
72
+
73
+ const home = await page('/')
74
+ if (productBlocks(home.html).length) errors.push('the home page must not carry a Product block')
75
+
76
+ if (errors.length) {
77
+ for (const e of errors) console.error(`error ${e}`)
78
+ process.exit(1)
79
+ }
80
+ console.log('✓ product page ships one valid schema.org Product/Offer JSON-LD block in the server html; home has none')
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `pnpm check` — validate this BUILT theme against Tanqory theme spec v1.
4
+ *
5
+ * The theme owns all of its code, so it carries its own copy of the spec
6
+ * checker (the platform keeps the canonical one in `packages/theme-spec`);
7
+ * a clone of this repository alone can run every gate.
8
+ */
9
+ import { readFile, stat, access } from 'node:fs/promises'
10
+ import { join, resolve } from 'node:path'
11
+ import { pathToFileURL } from 'node:url'
12
+
13
+ const SPEC_VERSION = 1
14
+
15
+ const CANVAS_PROTOCOL = Object.freeze({
16
+ fromTheme: Object.freeze(['tq:ready', 'tq:select', 'tanqory-section-selected', 'tanqory-block-selected', 'tanqory-content-changed', 'tanqory-content', 'tanqory-collections', 'tanqory-menus', 'tanqory-products', 'tq-preview-height']),
17
+ toTheme: Object.freeze(['tq:set-content', 'tq:select', 'tq:theme-settings', 'tanqory-preview-update-section', 'tanqory-preview-select', 'tanqory-preview-reorder-sections', 'tanqory-preview-remove-section', 'tanqory-preview-insert-section', 'tanqory-get-content', 'tanqory-request-collections', 'tanqory-request-menus', 'tanqory-request-products']),
18
+ })
19
+
20
+ const LIMITS = Object.freeze({
21
+ ssrBundleBytes: 10 * 1024 * 1024, // Workers script limit
22
+ clientBundleBytes: 25 * 1024 * 1024,
23
+ renderMs: 50, // CPU budget per render (isolate)
24
+ })
25
+
26
+ const SETTING_TYPES = new Set(['text', 'textarea', 'richtext', 'html', 'url', 'number', 'range', 'boolean', 'select', 'radio', 'color', 'text_alignment', 'image', 'video', 'collection', 'product', 'page', 'blog', 'article', 'menu', 'header', 'paragraph', 'link_list', 'inline_richtext', 'font_picker', 'checkbox'])
27
+
28
+ /** Errors in a manifest object; [] when valid. */
29
+ function validateManifest(m) {
30
+ const errors = []
31
+ if (!m || typeof m !== 'object') return ['manifest is not an object']
32
+ if (m.spec !== SPEC_VERSION) errors.push(`manifest.spec must be ${SPEC_VERSION}`)
33
+ if (typeof m.name !== 'string' || !m.name) errors.push('manifest.name is required')
34
+ if (!Array.isArray(m.sections)) errors.push('manifest.sections must be an array')
35
+ else {
36
+ const seen = new Set()
37
+ m.sections.forEach((s, i) => {
38
+ if (!s || typeof s.name !== 'string' || !/^[a-z0-9-]+$/.test(s.name)) { errors.push(`sections[${i}].name must be kebab-case`); return }
39
+ if (seen.has(s.name)) errors.push(`duplicate section "${s.name}"`)
40
+ seen.add(s.name)
41
+ if (s.role && !['layout', 'section', 'block'].includes(s.role)) errors.push(`sections[${i}] "${s.name}": role must be layout|section|block`)
42
+ for (const [id, spec] of Object.entries(s.attributes ?? {})) {
43
+ if (!spec || typeof spec.type !== 'string') errors.push(`section "${s.name}" attribute "${id}" has no type`)
44
+ else if (!SETTING_TYPES.has(spec.type)) errors.push(`section "${s.name}" attribute "${id}": unknown type "${spec.type}"`)
45
+ }
46
+ })
47
+ }
48
+ if (!Array.isArray(m.templates) || !m.templates.includes('index')) errors.push('manifest.templates must include "index"')
49
+ if (m.groups !== undefined && !Array.isArray(m.groups)) errors.push('manifest.groups must be an array')
50
+ return errors
51
+ }
52
+
53
+ const FORBIDDEN_IMPORTS = /(?:^|[^\w])(?:from|import)\s*\(?\s*["'](node:[a-z_]+|fs|path|os|child_process|crypto|net|http|https|worker_threads|vm)["']/g
54
+
55
+ /** Validate a BUILT theme directory. Never throws for a theme error — reports it. */
56
+ async function checkTheme(themeDir) {
57
+ const errors = [], warnings = []
58
+ const entry = join(themeDir, 'dist', 'ssr', 'entry.mjs')
59
+ const clientDir = join(themeDir, 'dist', 'client')
60
+ const exists = async (p) => { try { await access(p); return true } catch { return false } }
61
+ if (!(await exists(entry))) errors.push('dist/ssr/entry.mjs is missing — the theme must build an SSR entry that exports render() and manifest')
62
+ if (!(await exists(join(clientDir, '.vite', 'manifest.json')))) errors.push('dist/client/.vite/manifest.json is missing — build the client with `build.manifest: true`')
63
+ let manifest
64
+ if (errors.length === 0) {
65
+ const size = (await stat(entry)).size
66
+ if (size > LIMITS.ssrBundleBytes) errors.push(`dist/ssr/entry.mjs is ${size} bytes, over the ${LIMITS.ssrBundleBytes}-byte isolate limit`)
67
+ const src = await readFile(entry, 'utf8')
68
+ const bad = [...src.matchAll(FORBIDDEN_IMPORTS)].map((m) => m[1])
69
+ if (bad.length) errors.push(`SSR bundle imports Node built-ins (${[...new Set(bad)].join(', ')}) — not available in the isolate; bundle everything (ssr.noExternal) and avoid Node APIs`)
70
+ if (/\brequire\s*\(/.test(src)) warnings.push('SSR bundle contains require() — it must be pure ESM for the isolate')
71
+ try {
72
+ const mod = await import(pathToFileURL(entry).href)
73
+ if (typeof mod.render !== 'function') errors.push('entry does not export render()')
74
+ if (!mod.manifest || typeof mod.manifest !== 'object') errors.push('entry does not export manifest')
75
+ else { manifest = mod.manifest; errors.push(...validateManifest(manifest)) }
76
+ } catch (e) {
77
+ errors.push(`entry failed to load: ${e?.message || e}`)
78
+ }
79
+ }
80
+ return { ok: errors.length === 0, errors, warnings, manifest }
81
+ }
82
+
83
+ const dir = resolve(process.argv[2] || '.')
84
+ const r = await checkTheme(dir)
85
+ for (const w of r.warnings) console.log('warn ', w)
86
+ for (const e of r.errors) console.log('error', e)
87
+ if (r.ok) console.log(`✓ theme "${r.manifest.name}" v${r.manifest.version ?? '?'} passes spec v1 (${r.manifest.sections.length} sections, ${r.manifest.templates.length} templates)`)
88
+ process.exit(r.ok ? 0 : 1)
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `tanqory-theme contracts` — writes and checks the theme's contract locks,
4
+ * `.tanqory/contracts/`. Compiler-managed: theme authors never edit them.
5
+ *
6
+ * The approved design package (`*.dc.html`) carries its data in a
7
+ * `<script type="text/x-dc">` block whose `renderVals()` returns the tables the
8
+ * canvas renders. Executing that block is the only faithful way to read it.
9
+ * Each design family becomes one lock, `<theme>-<family>.json`: the persisted
10
+ * ids (sections, blocks, settings, options), labels, presets and shared
11
+ * settings. `LOCK.json` records, per lock, the design file it came from and the
12
+ * sha256 of both — so a hand edit, or a design change nobody re-locked, fails.
13
+ *
14
+ * tanqory-theme contracts --check locks match LOCK.json (no hand edits)
15
+ * tanqory-theme contracts --design <dir> --check …and still match the design package
16
+ * tanqory-theme contracts --design <dir> --write re-extract from the design package
17
+ */
18
+ import { createHash } from 'node:crypto'
19
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
20
+ import { join, resolve } from 'node:path'
21
+
22
+ const THEME = process.cwd()
23
+ const OUT = join(THEME, '.tanqory', 'contracts')
24
+ const LOCK = join(OUT, 'LOCK.json')
25
+ const args = process.argv.slice(2)
26
+ const WRITE = args.includes('--write')
27
+ const designAt = args.indexOf('--design')
28
+ const DESIGN = designAt >= 0 ? resolve(args[designAt + 1] ?? '') : null
29
+
30
+ /** The families a theme locks, by default (Nova's): design file → family id. */
31
+ const DEFAULT_SOURCES = {
32
+ '01': 'Nova 01 Style System.dc.html',
33
+ '12A': 'Nova 12A - Chrome and Structure.dc.html',
34
+ '12B': 'Nova 12B - Heroes and Promotion.dc.html',
35
+ '12C': 'Nova 12C - Product and Merchandising.dc.html',
36
+ '12D': 'Nova 12D - Content and Storytelling.dc.html',
37
+ '12E': 'Nova 12E - Editorial and Long-form.dc.html',
38
+ '12F': 'Nova 12F - Trust and Advanced Commerce.dc.html',
39
+ '12G': 'Nova 12G - Utility and Customer Experience.dc.html',
40
+ '13': 'Nova 13 - Overlays and Feedback.dc.html',
41
+ '14': 'Nova 14 - Media and System States.dc.html',
42
+ }
43
+
44
+ const sha256 = (text) => createHash('sha256').update(text).digest('hex')
45
+ const themeName = () => {
46
+ const name = JSON.parse(readFileSync(join(THEME, 'package.json'), 'utf8')).name ?? 'theme'
47
+ return name.split('/').pop()
48
+ }
49
+
50
+ // ── extraction (the design package → one JSON per family) ────────────────────
51
+
52
+ const DISPLAY_ONLY = new Set(['sbg', 'sfg', 'optDisp', 'legacyDisp', 'legacyLblDisp', 'defDisp', 'defRange', 'rowNames', 'counts'])
53
+
54
+ function renderVals(src, file) {
55
+ const m = src.match(/<script type="text\/x-dc"[^>]*>([\s\S]*?)<\/script>/)
56
+ if (!m) throw new Error(`${file}: no x-dc logic block`)
57
+ class DCLogic {
58
+ constructor() {
59
+ this.props = {}
60
+ this.state = {}
61
+ }
62
+ setState() {}
63
+ }
64
+ return new Function('DCLogic', `${m[1]}; return new Component().renderVals()`)(DCLogic)
65
+ }
66
+
67
+ /** `a · b · c` → ['a', 'b', 'c']; '' → []. */
68
+ const ids = (s) => (typeof s === 'string' && s.trim() ? s.split('·').map((x) => x.trim()).filter(Boolean) : [])
69
+
70
+ /** Booleans and numbers arrive as strings in some tables ("false", "32"). */
71
+ function normaliseDefault(row) {
72
+ const d = row.def
73
+ if (row.control === 'boolean') return d === true || d === 'true'
74
+ if (row.control === 'range' || row.control === 'number') return d === '' || d == null ? null : Number(d)
75
+ return d == null ? '' : d
76
+ }
77
+
78
+ function row(r) {
79
+ const out = {}
80
+ for (const [k, v] of Object.entries(r)) if (!DISPLAY_ONLY.has(k)) out[k] = v
81
+ out.options = ids(r.options)
82
+ out.optionLabels = ids(r.optionLabels)
83
+ out.legacyOptions = ids(r.legacyOptions)
84
+ out.legacyOptionLabels = ids(r.legacyOptionLabels)
85
+ out.def = normaliseDefault(r)
86
+ return out
87
+ }
88
+
89
+ /** A foundations table set (Nova 01) is kept whole, not as a section contract. */
90
+ const FOUNDATION_KEYS = ['tokens', 'colors', 'brandDerive', 'families', 'typeScale', 'typeRules', 'radii', 'radiusMap', 'gaps', 'sectionSpace', 'containers', 'ratios', 'motion', 'dark', 'principles']
91
+
92
+ function family(v) {
93
+ if (v.typeScale && v.radii) return Object.fromEntries(FOUNDATION_KEYS.map((k) => [k, v[k] ?? []]))
94
+ const out = {
95
+ sections: (v.sections ?? []).map((s) => ({ name: s.name, label: s.label, status: s.status, rows: s.rows.map(row) })),
96
+ blocks: (v.blocks ?? []).map((b) => ({
97
+ name: b.name,
98
+ label: b.label,
99
+ parents: [...new Set(b.parents ?? [])],
100
+ min: b.min,
101
+ recommended: b.recommended,
102
+ max: b.max,
103
+ status: b.status,
104
+ resp: b.resp,
105
+ empty: b.empty,
106
+ summary: b.summary ?? '',
107
+ rows: (b.rows ?? []).map(row),
108
+ })),
109
+ presets: v.presets ?? [],
110
+ shared: (v.shared ?? []).map(row),
111
+ sharedApplicability: v.sharedApplicability ?? [],
112
+ globals: (v.globals ?? []).map(row),
113
+ ai: v.ai ?? [],
114
+ conflicts: v.conflicts ?? [],
115
+ }
116
+ if (v.placeholders) out.placeholders = v.placeholders
117
+ if (v.stateFamilies) out.stateFamilies = v.stateFamilies
118
+ return out
119
+ }
120
+
121
+ // ── the lock ─────────────────────────────────────────────────────────────────
122
+
123
+ const readLock = () => (existsSync(LOCK) ? JSON.parse(readFileSync(LOCK, 'utf8')) : null)
124
+ const sourcesOf = (lock) => (lock ? Object.fromEntries(Object.values(lock.files).map((f) => [f.family, f.design])) : DEFAULT_SOURCES)
125
+
126
+ function extract(sources) {
127
+ const present = new Set(readdirSync(DESIGN))
128
+ const out = {}
129
+ for (const [id, file] of Object.entries(sources)) {
130
+ if (!present.has(file)) throw new Error(`${file} is not in ${DESIGN}`)
131
+ const design = readFileSync(join(DESIGN, file), 'utf8')
132
+ out[`${themeName()}-${id}.json`] = { family: id, design: file, designSha256: sha256(design), json: JSON.stringify(family(renderVals(design, file)), null, 2) + '\n' }
133
+ }
134
+ return out
135
+ }
136
+
137
+ function lockDoc(files) {
138
+ return {
139
+ $comment: 'Generated by `tanqory-theme contracts`. Do not edit — neither this file nor the locks beside it. Re-lock from the approved design package with `tanqory-theme contracts --design <dir> --write`.',
140
+ generator: '@tanqory/theme-compiler contracts',
141
+ files: Object.fromEntries(
142
+ Object.entries(files)
143
+ .sort(([a], [b]) => a.localeCompare(b))
144
+ .map(([name, f]) => [name, { family: f.family, design: f.design, designSha256: f.designSha256, sha256: sha256(f.json) }]),
145
+ ),
146
+ }
147
+ }
148
+
149
+ let failures = 0
150
+ const fail = (msg) => {
151
+ console.error(`✗ ${msg}`)
152
+ failures++
153
+ }
154
+
155
+ if (WRITE) {
156
+ if (!DESIGN) {
157
+ console.error('usage: tanqory-theme contracts --design <dir> --write')
158
+ process.exit(2)
159
+ }
160
+ const files = extract(sourcesOf(readLock()))
161
+ mkdirSync(OUT, { recursive: true })
162
+ for (const [name, f] of Object.entries(files)) writeFileSync(join(OUT, name), f.json)
163
+ writeFileSync(LOCK, JSON.stringify(lockDoc(files), null, 2) + '\n')
164
+ console.log(`✓ .tanqory/contracts — ${Object.keys(files).length} locks written from ${DESIGN}`)
165
+ } else {
166
+ const lock = readLock()
167
+ if (!lock) {
168
+ console.error('✗ .tanqory/contracts/LOCK.json is missing — run `tanqory-theme contracts --design <dir> --write`')
169
+ process.exit(1)
170
+ }
171
+ // 1. No hand edits: every lock matches the hash the compiler recorded.
172
+ const onDisk = new Set(readdirSync(OUT).filter((f) => f.endsWith('.json') && f !== 'LOCK.json'))
173
+ for (const [name, f] of Object.entries(lock.files)) {
174
+ if (!onDisk.has(name)) fail(`${name} is missing`)
175
+ else if (sha256(readFileSync(join(OUT, name), 'utf8')) !== f.sha256) fail(`${name} was edited by hand — locks are compiler-managed`)
176
+ onDisk.delete(name)
177
+ }
178
+ for (const name of onDisk) fail(`${name} is not in LOCK.json — locks are compiler-managed`)
179
+ // 2. With the design package at hand: the locks still say what it says.
180
+ if (DESIGN) {
181
+ const files = extract(sourcesOf(lock))
182
+ for (const [name, f] of Object.entries(files)) {
183
+ if (lock.files[name]?.sha256 !== sha256(f.json)) fail(`${name} no longer matches ${f.design} — re-lock with --write`)
184
+ }
185
+ }
186
+ if (failures) process.exit(1)
187
+ console.log(`✓ .tanqory/contracts — ${Object.keys(lock.files).length} locks intact${DESIGN ? ' and match the design package' : ''}`)
188
+ }