@thorprovider/storefront 0.3.1 → 0.4.0-mvp.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 (263) hide show
  1. package/README.md +321 -152
  2. package/VENDOR.md +69 -0
  3. package/assets/openspec/config.yaml +38 -0
  4. package/assets/templates/AGENTS.md +76 -0
  5. package/assets/templates/app/app/[[...slug]]/page.tsx +54 -2
  6. package/assets/templates/app/app/[[...slug]]/renderer.tsx +13 -2
  7. package/assets/templates/app/app/globals.css +5 -8
  8. package/assets/templates/app/app/layout.tsx +17 -7
  9. package/assets/templates/app/app/robots.ts +19 -0
  10. package/assets/templates/app/app/sitemap.ts +71 -0
  11. package/assets/templates/app/lib/__STOREFRONT__/actions.ts +612 -0
  12. package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
  13. package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +50 -57
  14. package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +679 -0
  15. package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
  16. package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +197 -0
  17. package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
  18. package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
  19. package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
  20. package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +264 -51
  21. package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +6 -3
  22. package/assets/templates/app/lib/__STOREFRONT__/schema.ts +136 -0
  23. package/assets/templates/app/lib/__STOREFRONT__/section-observation.ts +182 -0
  24. package/assets/templates/app/lib/__STOREFRONT__/section-sources.ts +130 -0
  25. package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
  26. package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
  27. package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +120 -0
  28. package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +38 -0
  29. package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +129 -10
  30. package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
  31. package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +191 -0
  32. package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
  33. package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
  34. package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +161 -0
  35. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
  36. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
  37. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
  38. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
  39. package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +18 -0
  40. package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
  41. package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
  42. package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.client.tsx +100 -0
  43. package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.tsx +19 -0
  44. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
  45. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
  46. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
  47. package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
  48. package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
  49. package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
  50. package/assets/templates/features/checkout/handlers.ts +159 -0
  51. package/assets/templates/features/checkout/state.ts +61 -0
  52. package/assets/templates/features/customer-auth/handlers.ts +59 -0
  53. package/assets/templates/features/customer-auth/state.ts +45 -0
  54. package/assets/templates/features/recommendations/state.ts +11 -0
  55. package/assets/templates/views/account/content.ts +35 -0
  56. package/assets/templates/views/account/index.tsx +114 -0
  57. package/assets/templates/views/account/registry.tsx +439 -0
  58. package/assets/templates/views/account/schema.ts +53 -0
  59. package/assets/templates/views/cart/content.ts +11 -0
  60. package/assets/templates/views/cart/data.ts +23 -0
  61. package/assets/templates/views/cart/index.tsx +53 -0
  62. package/assets/templates/views/cart/props.ts +12 -0
  63. package/assets/templates/views/cart/registry.tsx +81 -0
  64. package/assets/templates/views/cart/schema.ts +52 -0
  65. package/assets/templates/views/category/assembly/assembly.json +29 -0
  66. package/assets/templates/views/category/assembly/index.skeleton.tsx +251 -0
  67. package/assets/templates/views/category/assembly/registry.skeleton.tsx +34 -0
  68. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/binding.json +5 -0
  69. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/catalog.part +4 -0
  70. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/element.part +7 -0
  71. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/registry.part +18 -0
  72. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
  73. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
  74. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
  75. package/assets/templates/views/category/content.ts +30 -0
  76. package/assets/templates/views/category/data.ts +12 -0
  77. package/assets/templates/views/category/index.tsx +299 -0
  78. package/assets/templates/views/category/props.ts +23 -0
  79. package/assets/templates/views/category/registry.tsx +122 -0
  80. package/assets/templates/views/category/schema.ts +91 -0
  81. package/assets/templates/views/checkout/content.ts +105 -0
  82. package/assets/templates/views/checkout/data.ts +23 -0
  83. package/assets/templates/views/checkout/index.tsx +111 -0
  84. package/assets/templates/views/checkout/props.ts +49 -0
  85. package/assets/templates/views/checkout/registry.tsx +217 -0
  86. package/assets/templates/views/checkout/schema.ts +229 -0
  87. package/assets/templates/views/home/assembly/assembly.json +33 -0
  88. package/assets/templates/views/home/assembly/index.skeleton.tsx +90 -0
  89. package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
  90. package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
  91. package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
  92. package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
  93. package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
  94. package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
  95. package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
  96. package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
  97. package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
  98. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
  99. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
  100. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
  101. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
  102. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
  103. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
  104. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
  105. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
  106. package/assets/templates/views/home/content.ts +70 -0
  107. package/assets/templates/views/home/data.ts +24 -0
  108. package/assets/templates/views/home/index.tsx +57 -85
  109. package/assets/templates/views/home/props.ts +22 -0
  110. package/assets/templates/views/home/registry.tsx +38 -22
  111. package/assets/templates/views/home/schema.ts +93 -0
  112. package/assets/templates/views/product/content.ts +25 -0
  113. package/assets/templates/views/product/data.ts +50 -0
  114. package/assets/templates/views/product/index.tsx +148 -0
  115. package/assets/templates/views/product/props.ts +27 -0
  116. package/assets/templates/views/product/registry.tsx +103 -0
  117. package/assets/templates/views/product/schema.ts +121 -0
  118. package/assets/templates/views/static/content.ts +99 -0
  119. package/assets/templates/views/static/index.tsx +60 -0
  120. package/assets/templates/views/static/registry.tsx +66 -0
  121. package/assets/templates/views/static/schema.ts +45 -0
  122. package/bin/storefront.js +2424 -148
  123. package/examples/merchandising/category-led.json +37 -0
  124. package/examples/merchandising/collection-led.json +37 -0
  125. package/package.json +15 -9
  126. package/recipes/archetypes.json +28 -14
  127. package/recipes/catalog.json +400 -0
  128. package/recipes/catalog.mjs +128 -0
  129. package/recipes/catalog.schema.json +64 -0
  130. package/recipes/feature.schema.json +89 -0
  131. package/recipes/features.json +162 -0
  132. package/recipes/manifest.schema.json +69 -0
  133. package/recipes/operations.schema.json +43 -0
  134. package/recipes/recipe.schema.json +11 -2
  135. package/recipes/recipes.json +76 -19
  136. package/recipes/sections.json +34 -5
  137. package/recipes/validate.mjs +838 -32
  138. package/scripts/candidate.mjs +236 -0
  139. package/scripts/composers.mjs +160 -0
  140. package/scripts/env-file.mjs +86 -0
  141. package/scripts/file-classes.mjs +121 -0
  142. package/scripts/logical-controls.mjs +61 -0
  143. package/scripts/manifest.mjs +298 -0
  144. package/scripts/migrate-english-routes.mjs +276 -0
  145. package/scripts/operation-registry.mjs +256 -0
  146. package/scripts/operations.mjs +720 -0
  147. package/scripts/ownership-conflict.mjs +356 -0
  148. package/scripts/pinning.mjs +573 -0
  149. package/scripts/plan-apply.mjs +987 -0
  150. package/scripts/preflight.mjs +51 -0
  151. package/scripts/reconcile.mjs +549 -0
  152. package/scripts/render-templates.mjs +338 -68
  153. package/scripts/schema-validate.mjs +132 -0
  154. package/scripts/section-compat.mjs +300 -0
  155. package/scripts/section-config.mjs +253 -0
  156. package/scripts/section-operations.mjs +508 -0
  157. package/scripts/starter.mjs +17 -0
  158. package/scripts/sync-assets.mjs +22 -15
  159. package/scripts/theme/color.mjs +185 -0
  160. package/scripts/theme/css-theme.mjs +233 -0
  161. package/scripts/theme/fonts.mjs +123 -0
  162. package/scripts/theme/import-export.mjs +125 -0
  163. package/scripts/theme/preset.mjs +93 -0
  164. package/scripts/theme/theme-plan.mjs +292 -0
  165. package/scripts/theme/tokens.mjs +137 -0
  166. package/scripts/view-assembly.mjs +337 -0
  167. package/scripts/view-selection.mjs +211 -0
  168. package/skills/sf-feature/SKILL.md +80 -0
  169. package/skills/sf-setup/SKILL.md +79 -0
  170. package/skills/sf-theme-gen/SKILL.md +100 -30
  171. package/skills/sf-theme-gen/references/shadcn-theming.md +87 -0
  172. package/skills/{shadcn-theming/references/templates → sf-theme-gen/references}/theme-setup.md +15 -19
  173. package/skills/{shadcn-theming → sf-theme-gen}/references/theming-guide.md +6 -11
  174. package/skills/sf-view-gen/SKILL.md +146 -93
  175. package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -14
  176. package/skills/sf-view-gen/assets/recipes/catalog.json +400 -0
  177. package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
  178. package/skills/sf-view-gen/assets/recipes/features.json +162 -0
  179. package/skills/sf-view-gen/assets/recipes/recipes.json +76 -19
  180. package/skills/sf-view-gen/assets/recipes/sections.json +34 -5
  181. package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
  182. package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
  183. package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
  184. package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
  185. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
  186. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
  187. package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
  188. package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
  189. package/skills/sf-view-gen/assets/templates/views/sections/products.client.tsx +100 -0
  190. package/skills/sf-view-gen/assets/templates/views/sections/products.tsx +19 -0
  191. package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
  192. package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
  193. package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
  194. package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
  195. package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
  196. package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
  197. package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
  198. package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
  199. package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
  200. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
  201. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
  202. package/skills/{json-render-core/SKILL.md → sf-view-gen/references/json-render-core.md} +4 -27
  203. package/skills/{json-render-next/SKILL.md → sf-view-gen/references/json-render-next.md} +5 -8
  204. package/skills/{json-render-react/SKILL.md → sf-view-gen/references/json-render-react.md} +3 -5
  205. package/skills/{json-render-shadcn/SKILL.md → sf-view-gen/references/json-render-shadcn.md} +3 -5
  206. package/skills/sf-view-gen/references/views.md +58 -33
  207. package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
  208. package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
  209. package/skills/sf-view-gen/scripts/validate.mjs +838 -32
  210. package/skills/storefront-workflow/SKILL.md +179 -0
  211. package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
  212. package/CHANGELOG.md +0 -61
  213. package/commands/sf-add-view.md +0 -26
  214. package/commands/sf-theme.md +0 -20
  215. package/commands/sf-view.md +0 -25
  216. package/skills/building-storefronts/SKILL.md +0 -178
  217. package/skills/building-storefronts/references/frontend-integration.md +0 -229
  218. package/skills/json-render-remotion/SKILL.md +0 -111
  219. package/skills/json-render-solid/SKILL.md +0 -204
  220. package/skills/nextjs-shadcn/SKILL.md +0 -367
  221. package/skills/nextjs-shadcn/references/architecture.md +0 -499
  222. package/skills/nextjs-shadcn/references/project-setup.md +0 -162
  223. package/skills/nextjs-shadcn/references/shadcn-platform.md +0 -261
  224. package/skills/nextjs-shadcn/references/sidebar.md +0 -274
  225. package/skills/nextjs-shadcn/references/styling.md +0 -555
  226. package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
  227. package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
  228. package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
  229. package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
  230. package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
  231. package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
  232. package/skills/sf-view-gen/assets/templates/views/user-panel/index.tsx +0 -168
  233. package/skills/sf-view-gen/assets/templates/views/user-panel/registry.tsx +0 -49
  234. package/skills/shadcn-component-discovery/SKILL.md +0 -273
  235. package/skills/shadcn-component-discovery/references/registries.md +0 -226
  236. package/skills/shadcn-theming/SKILL.md +0 -104
  237. package/skills/storefront-best-practices/SKILL.md +0 -421
  238. package/skills/storefront-best-practices/reference/components/breadcrumbs.md +0 -123
  239. package/skills/storefront-best-practices/reference/components/cart-popup.md +0 -189
  240. package/skills/storefront-best-practices/reference/components/country-selector.md +0 -298
  241. package/skills/storefront-best-practices/reference/components/footer.md +0 -112
  242. package/skills/storefront-best-practices/reference/components/hero.md +0 -241
  243. package/skills/storefront-best-practices/reference/components/megamenu.md +0 -239
  244. package/skills/storefront-best-practices/reference/components/navbar.md +0 -397
  245. package/skills/storefront-best-practices/reference/components/popups.md +0 -221
  246. package/skills/storefront-best-practices/reference/components/product-card.md +0 -125
  247. package/skills/storefront-best-practices/reference/components/product-reviews.md +0 -217
  248. package/skills/storefront-best-practices/reference/components/product-slider.md +0 -174
  249. package/skills/storefront-best-practices/reference/components/search.md +0 -101
  250. package/skills/storefront-best-practices/reference/connecting-to-backend.md +0 -391
  251. package/skills/storefront-best-practices/reference/features/promotions.md +0 -307
  252. package/skills/storefront-best-practices/reference/features/wishlist.md +0 -230
  253. package/skills/storefront-best-practices/reference/layouts/account.md +0 -380
  254. package/skills/storefront-best-practices/reference/layouts/cart.md +0 -316
  255. package/skills/storefront-best-practices/reference/layouts/checkout.md +0 -486
  256. package/skills/storefront-best-practices/reference/layouts/home-page.md +0 -264
  257. package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +0 -231
  258. package/skills/storefront-best-practices/reference/layouts/product-details.md +0 -527
  259. package/skills/storefront-best-practices/reference/layouts/product-listing.md +0 -520
  260. package/skills/storefront-best-practices/reference/layouts/static-pages.md +0 -356
  261. package/skills/storefront-best-practices/reference/medusa.md +0 -307
  262. /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/mobile-responsiveness.md +0 -0
  263. /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/seo.md +0 -0
@@ -0,0 +1,111 @@
1
+ // Thor Commerce view module: checkout
2
+ // Server Component module: elements + catalog entries + route + loader only.
3
+ // The client-side component mappings live in ./registry.tsx ('use client'); this
4
+ // module imports them as data so the RSC graph never evaluates @json-render/react
5
+ // on the server. Registered by importing it in ../index.ts. Recipe: recipes.json -> "checkout".
6
+ //
7
+ // The initial render does NOT read cookies here (this module is part of the
8
+ // client catalog graph). The catch-all page injects the read-only cart state;
9
+ // `CheckoutBoot` then loads the checkout data or reconciles a completed receipt
10
+ // through Server Actions.
11
+
12
+ import { getDataSource } from '../../store-context'
13
+ import { pageRoot } from '../../spec/page-root'
14
+ import type { StorefrontElement } from '../../spec/types'
15
+ import type { ViewModule } from '../types'
16
+ import { registry } from './registry'
17
+ import { checkoutConfirmationProps, checkoutViewProps } from './props'
18
+ import {
19
+ checkoutConfirmationPropsSchema,
20
+ checkoutEmptyPropsSchema,
21
+ checkoutPropsSchema,
22
+ } from './schema'
23
+
24
+ // The element tree is derived wiring; the rendered props and validation rules
25
+ // come from the user-owned ./props.ts module so they survive sync and removal.
26
+ // Four states: bootstrap (headless), confirmation (order), empty cart, or form.
27
+ const elements: Record<string, StorefrontElement> = {
28
+ 'checkout.boot': {
29
+ type: 'CheckoutBoot',
30
+ props: {},
31
+ },
32
+ 'checkout.confirmation': {
33
+ type: 'CheckoutConfirmation',
34
+ visible: { $state: '/lastReceipt' },
35
+ props: checkoutConfirmationProps,
36
+ on: { continueShopping: { action: 'continueShopping' } },
37
+ },
38
+ 'checkout.empty': {
39
+ type: 'CheckoutEmpty',
40
+ visible: {
41
+ $and: [
42
+ { $state: '/lastReceipt', not: true },
43
+ { $state: '/cartQuantity', eq: 0 },
44
+ // Show the empty state when there is no active cart and no receipt to
45
+ // recover, or when an active cart exists (even empty). A signed receipt
46
+ // with no active cart is reconciled by the boot, not shown as empty.
47
+ {
48
+ $or: [{ $state: '/receiptAvailable', not: true }, { $state: '/cartId' }],
49
+ },
50
+ ],
51
+ },
52
+ props: {},
53
+ on: { continueShopping: { action: 'continueShopping' } },
54
+ },
55
+ 'checkout.view': {
56
+ type: 'Checkout',
57
+ visible: {
58
+ $and: [{ $state: '/lastReceipt', not: true }, { $state: '/cartQuantity', gt: 0 }],
59
+ },
60
+ props: checkoutViewProps,
61
+ on: {
62
+ placeOrder: { action: 'placeOrder' },
63
+ reconcileOrder: { action: 'reconcileOrder' },
64
+ syncCheckoutAddress: { action: 'syncCheckoutAddress' },
65
+ selectShippingMethod: { action: 'selectShippingMethod' },
66
+ },
67
+ },
68
+ }
69
+
70
+ const catalog = {
71
+ CheckoutBoot: {
72
+ props: checkoutEmptyPropsSchema,
73
+ description: 'Headless checkout bootstrap: loads form data or reconciles a receipt.',
74
+ },
75
+ Checkout: {
76
+ props: checkoutPropsSchema,
77
+ description: 'Checkout form with order summary, shipping and payment steps.',
78
+ },
79
+ CheckoutEmpty: {
80
+ props: checkoutEmptyPropsSchema,
81
+ description: 'Empty-cart state for the checkout route.',
82
+ },
83
+ CheckoutConfirmation: {
84
+ props: checkoutConfirmationPropsSchema,
85
+ description: 'Order confirmation with fulfilment progress.',
86
+ },
87
+ }
88
+
89
+ export const checkout: ViewModule = {
90
+ name: 'checkout',
91
+ elements,
92
+ catalog,
93
+ registry,
94
+ route: {
95
+ path: '/checkout',
96
+ loaderName: 'checkout',
97
+ layout: 'default',
98
+ metadata: {
99
+ title: 'Finalizar compra',
100
+ description: 'Completa tus datos de envío y pago para recibir tu pedido.',
101
+ },
102
+ entry: pageRoot(
103
+ 'checkout',
104
+ ['checkout.boot', 'checkout.confirmation', 'checkout.empty', 'checkout.view'],
105
+ elements,
106
+ ),
107
+ },
108
+ loader: {
109
+ checkout: async () => ({ dataSource: getDataSource(), dataError: null }),
110
+ },
111
+ }
@@ -0,0 +1,49 @@
1
+ // Thor Commerce view props: checkout
2
+ // User-owned element props for the checkout view. Reconcile seeds this file once
3
+ // and never rewrites it; the derived view module (./index.tsx) imports these
4
+ // objects and binds them into the element tree, so edits here survive
5
+ // `storefront sync` and unit removal. Plain data only: no React and no server
6
+ // imports.
7
+
8
+ import { checkoutContent, confirmationContent } from './content'
9
+
10
+ export const checkoutViewProps = {
11
+ ...checkoutContent,
12
+ // Backend-authoritative summary: line totals and cart totals come from the
13
+ // backend cart state, never computed here.
14
+ orderSummary: {
15
+ items: { $state: '/cart' },
16
+ totals: { $state: '/cartTotals' },
17
+ },
18
+ countries: { $state: '/checkoutCountries' },
19
+ shippingOptions: { $state: '/checkoutShippingOptions' },
20
+ selectedShippingOptionId: { $state: '/checkoutSelectedShippingOptionId' },
21
+ paymentMethods: { $state: '/checkoutPaymentMethods' },
22
+ fieldErrors: { $state: '/checkoutFieldErrors' },
23
+ values: {
24
+ email: { $state: '/checkout/email' },
25
+ firstName: { $state: '/checkout/firstName' },
26
+ lastName: { $state: '/checkout/lastName' },
27
+ phone: { $state: '/checkout/phone' },
28
+ address: { $state: '/checkout/address' },
29
+ city: { $state: '/checkout/city' },
30
+ state: { $state: '/checkout/state' },
31
+ zipCode: { $state: '/checkout/zipCode' },
32
+ country: { $state: '/checkout/country' },
33
+ paymentMethod: { $state: '/checkout/paymentMethod' },
34
+ cardNumber: { $state: '/checkout/cardNumber' },
35
+ expiryDate: { $state: '/checkout/expiryDate' },
36
+ cvv: { $state: '/checkout/cvv' },
37
+ cardName: { $state: '/checkout/cardName' },
38
+ saveInfo: { $state: '/checkout/saveInfo' },
39
+ sameAsBilling: { $state: '/checkout/sameAsBilling' },
40
+ newsletter: { $state: '/checkout/newsletter' },
41
+ promoCode: { $state: '/checkout/promoCode' },
42
+ },
43
+ }
44
+
45
+ export const checkoutConfirmationProps = {
46
+ // One atomic state path carries the order and its backend lines together.
47
+ receipt: { $state: '/lastReceipt' },
48
+ ...confirmationContent,
49
+ }
@@ -0,0 +1,217 @@
1
+ 'use client'
2
+
3
+ // Thor Commerce view registry fragment: checkout
4
+ // Client boundary for the checkout view. Imports value bindings from
5
+ // @json-render/react and components from @thorprovider/blocks — both must stay
6
+ // on the client side of the RSC graph. Kept separate from index.tsx so the view
7
+ // spec (elements/catalog/route/loader) stays a pure Server Component module.
8
+
9
+ import { useEffect, useRef } from 'react'
10
+ import {
11
+ Button,
12
+ CheckoutForm1 as CheckoutBlock,
13
+ CheckoutConfirmation as CheckoutConfirmationBlock,
14
+ } from '@thorprovider/blocks'
15
+ import { useStateStore } from '@json-render/react'
16
+ import { loadCheckoutAction, reconcileReceiptAction } from '../../actions'
17
+ import type { CartStatePatch, PlaceOrderResult } from '../../commerce'
18
+ import type { ViewRegistry } from '../types'
19
+ import { checkoutConfirmationPropsSchema, checkoutPropsSchema } from './schema'
20
+
21
+ // Address fields whose change can complete the address; when the full address
22
+ // is present the handler syncs it to the backend and loads eligible shipping.
23
+ const ADDRESS_FIELDS = new Set([
24
+ 'email',
25
+ 'firstName',
26
+ 'lastName',
27
+ 'phone',
28
+ 'address',
29
+ 'city',
30
+ 'state',
31
+ 'zipCode',
32
+ 'country',
33
+ ])
34
+
35
+ export const registry: ViewRegistry = {
36
+ /**
37
+ * Headless bootstrap rendered on the checkout route. It loads the checkout
38
+ * data (countries/payment methods/cart) for an active cart, or reconciles a
39
+ * completed receipt through the cookie-authorized Server Action. It never
40
+ * reads a bare order/cart id.
41
+ */
42
+ CheckoutBoot: () => {
43
+ const { state, set } = useStateStore()
44
+ const booted = useRef(false)
45
+ useEffect(() => {
46
+ if (booted.current) return
47
+ booted.current = true
48
+
49
+ const applyResult = (result: PlaceOrderResult) => {
50
+ set('/checkoutPending', false)
51
+ if (result.status === 'order') {
52
+ // ONE atomic write carries the order and its backend lines, so an SSR
53
+ // refresh cannot drop the lines.
54
+ set('/lastReceipt', { order: result.order, lines: result.lines })
55
+ set('/orderStatus', 'complete')
56
+ set('/checkoutError', null)
57
+ set('/checkoutErrorKind', null)
58
+ set('/cart', [])
59
+ set('/cartTotals', null)
60
+ set('/cartId', null)
61
+ set('/cartQuantity', 0)
62
+ set('/cartCompleted', true)
63
+ set('/checkoutSelectedShippingOptionId', null)
64
+ return
65
+ }
66
+ set('/orderStatus', result.status === 'cart-error' ? 'error' : result.status)
67
+ set('/checkoutErrorKind', result.status)
68
+ set('/checkoutError', result.message)
69
+ }
70
+
71
+ const bootError = (scope: string) => (error: unknown) => {
72
+ // Never swallow: log a non-secret context line and surface a message.
73
+ console.error(
74
+ `[storefront/checkout] ${scope} failed`,
75
+ error instanceof Error ? error.message : error,
76
+ )
77
+ set('/checkoutPending', false)
78
+ set('/checkoutError', 'No se pudo cargar el checkout. Inténtalo de nuevo.')
79
+ set('/checkoutErrorKind', 'error')
80
+ }
81
+
82
+ // 1. An active cart the backend already committed is an unresolved
83
+ // completion: reconcile IT first (never a blind retry).
84
+ // 2. Otherwise ANY active cart (even empty) loads the checkout form; the
85
+ // signed receipt is NOT auto-recovered while an active cart exists.
86
+ // 3. Only with no active cart does a signed receipt proof recover the
87
+ // completed order.
88
+ if (state.cartId && state.cartCompleted) {
89
+ set('/checkoutPending', true)
90
+ void reconcileReceiptAction().then(applyResult).catch(bootError('reconcileReceipt'))
91
+ return
92
+ }
93
+ if (state.cartId) {
94
+ set('/checkoutPending', true)
95
+ void loadCheckoutAction()
96
+ .then((result) => {
97
+ set('/checkoutPending', false)
98
+ if (!result.ok) {
99
+ set('/checkoutError', result.error)
100
+ set('/checkoutErrorKind', 'error')
101
+ return
102
+ }
103
+ const cart = result.cart as CartStatePatch
104
+ set('/cart', cart.cart)
105
+ set('/cartTotals', cart.cartTotals)
106
+ set('/cartId', cart.cartId)
107
+ set('/cartQuantity', cart.cartQuantity)
108
+ set('/cartSelectedShippingOptionId', cart.cartSelectedShippingOptionId)
109
+ set('/checkoutSelectedShippingOptionId', cart.cartSelectedShippingOptionId)
110
+ if (result.countries) set('/checkoutCountries', result.countries)
111
+ if (result.paymentMethods) set('/checkoutPaymentMethods', result.paymentMethods)
112
+ if (result.shippingOptions) set('/checkoutShippingOptions', result.shippingOptions)
113
+ set('/checkoutError', null)
114
+ })
115
+ .catch(bootError('loadCheckout'))
116
+ return
117
+ }
118
+ if (state.receiptAvailable) {
119
+ set('/checkoutPending', true)
120
+ void reconcileReceiptAction().then(applyResult).catch(bootError('reconcileReceipt'))
121
+ }
122
+ }, [])
123
+ return null
124
+ },
125
+ Checkout: ({ props, emit }) => {
126
+ const { state, set } = useStateStore()
127
+ const parsed = checkoutPropsSchema.parse(props)
128
+ const unresolved = state.checkoutErrorKind === 'unresolved'
129
+ const cartError = typeof state.cartError === 'string' ? state.cartError : undefined
130
+ const checkoutError = typeof state.checkoutError === 'string' ? state.checkoutError : undefined
131
+ return (
132
+ <CheckoutBlock
133
+ title={parsed.title}
134
+ description={parsed.description}
135
+ orderSummary={parsed.orderSummary}
136
+ totalsState={state.cartPending ? 'pending' : cartError ? 'error' : 'idle'}
137
+ totalsErrorLabel={cartError}
138
+ onRetryTotals={() => emit('syncCheckoutAddress')}
139
+ countries={parsed.countries}
140
+ shippingOptions={parsed.shippingOptions}
141
+ selectedShippingOptionId={parsed.selectedShippingOptionId ?? undefined}
142
+ onSelectShippingOption={(id) => {
143
+ set('/checkoutSelectedShippingOptionId', id)
144
+ emit('selectShippingMethod')
145
+ }}
146
+ paymentMethods={parsed.paymentMethods}
147
+ values={parsed.values}
148
+ fieldErrors={parsed.fieldErrors}
149
+ fieldDescriptions={parsed.fieldDescriptions}
150
+ collectCardFields={false}
151
+ onChange={(field, value) => {
152
+ set(`/checkout/${field}`, value)
153
+ if (ADDRESS_FIELDS.has(field)) emit('syncCheckoutAddress')
154
+ }}
155
+ onSubmit={() => emit('placeOrder')}
156
+ submitting={Boolean(state.checkoutPending)}
157
+ submitError={checkoutError}
158
+ // An ambiguous completion offers RECONCILE only; a definite failure
159
+ // (cart-with-error or pre-completion error) offers a controlled retry.
160
+ onRetrySubmit={unresolved ? () => emit('reconcileOrder') : () => emit('placeOrder')}
161
+ submitErrorActionLabel={unresolved ? 'Reconciliar' : 'Reintentar'}
162
+ />
163
+ )
164
+ },
165
+ CheckoutEmpty: ({ emit }) => (
166
+ <div className="mx-auto max-w-md px-4 py-16 text-center">
167
+ <h1 className="text-2xl font-bold">Tu carrito está vacío</h1>
168
+ <p className="text-muted-foreground mt-2 text-sm">
169
+ Agrega productos para finalizar la compra.
170
+ </p>
171
+ <Button className="mt-6" onClick={() => emit('continueShopping')}>
172
+ Seguir comprando
173
+ </Button>
174
+ </div>
175
+ ),
176
+ CheckoutConfirmation: ({ props, emit }) => {
177
+ const parsed = checkoutConfirmationPropsSchema.parse(props)
178
+ // Forward every neutral label from user-owned content to the block, so the
179
+ // curated flow never falls back to the block's non-neutral defaults.
180
+ return (
181
+ <CheckoutConfirmationBlock
182
+ order={parsed.receipt.order}
183
+ orderLines={parsed.receipt.lines}
184
+ fulfilmentDetails={parsed.fulfilmentDetails}
185
+ titleLabel={parsed.titleLabel}
186
+ orderPlacedLabel={parsed.orderPlacedLabel}
187
+ orderPlacedNoEmailLabel={parsed.orderPlacedNoEmailLabel}
188
+ itemsLabel={parsed.itemsLabel}
189
+ quantityLabel={parsed.quantityLabel}
190
+ subtotalLabel={parsed.subtotalLabel}
191
+ discountLabel={parsed.discountLabel}
192
+ shippingLabel={parsed.shippingLabel}
193
+ taxLabel={parsed.taxLabel}
194
+ totalLabel={parsed.totalLabel}
195
+ freeLabel={parsed.freeLabel}
196
+ amountUnavailableLabel={parsed.amountUnavailableLabel}
197
+ shippingAddressLabel={parsed.shippingAddressLabel}
198
+ billingAddressLabel={parsed.billingAddressLabel}
199
+ continueShoppingLabel={parsed.continueShoppingLabel}
200
+ contactSupportLabel={parsed.contactSupportLabel}
201
+ supportLabel={parsed.supportLabel}
202
+ progressLabel={parsed.progressLabel}
203
+ estimatedDeliveryLabel={parsed.estimatedDeliveryLabel}
204
+ currentStageLabel={parsed.currentStageLabel}
205
+ inProgressLabel={parsed.inProgressLabel}
206
+ expectedByLabel={parsed.expectedByLabel}
207
+ completedLabel={parsed.completedLabel}
208
+ statusLabel={parsed.statusLabel}
209
+ locationLabel={parsed.locationLabel}
210
+ trackOrderLabel={parsed.trackOrderLabel}
211
+ continueShoppingHref={parsed.continueShoppingHref ?? '#'}
212
+ supportHref={parsed.supportHref}
213
+ onContinueShopping={() => emit('continueShopping')}
214
+ />
215
+ )
216
+ },
217
+ }
@@ -0,0 +1,229 @@
1
+ // Thor Commerce view schema fragment: checkout
2
+ // Zod mirrors of the block contracts for this view — the single source for the
3
+ // catalog entries and the client-side registry parsing (json-render does not
4
+ // validate props against the catalog itself, so the registry parses here).
5
+ // Kept free of React and server imports so the Server Component module and the
6
+ // 'use client' registry can share one contract. Block zod contracts are
7
+ // mirrored, not imported: @thorprovider/blocks ships zod v3 while the scaffold
8
+ // runs zod v4 and cross-major schema assignment breaks the catalog contract.
9
+
10
+ import { z } from 'zod'
11
+
12
+ // Canonical Money: a decimal string in major units, never cents.
13
+ const moneySchema = z.object({
14
+ amount: z.string(),
15
+ currencyCode: z.string(),
16
+ })
17
+
18
+ // Mirrors the CheckoutForm1FormData block contract. Card fields stay in the
19
+ // shape (the block requires every key) but are never collected at launch.
20
+ export const checkoutFormDataSchema = z.object({
21
+ email: z.string(),
22
+ firstName: z.string(),
23
+ lastName: z.string(),
24
+ phone: z.string(),
25
+ address: z.string(),
26
+ city: z.string(),
27
+ state: z.string(),
28
+ zipCode: z.string(),
29
+ country: z.string(),
30
+ paymentMethod: z.string(),
31
+ cardNumber: z.string(),
32
+ expiryDate: z.string(),
33
+ cvv: z.string(),
34
+ cardName: z.string(),
35
+ saveInfo: z.boolean(),
36
+ sameAsBilling: z.boolean(),
37
+ newsletter: z.boolean(),
38
+ promoCode: z.string(),
39
+ })
40
+
41
+ // Mirrors CheckoutForm1OrderSummaryItem: backend line total preferred, legacy
42
+ // numeric price accepted only as a fallback; optional image omitted when absent.
43
+ export const orderSummaryItemSchema = z.object({
44
+ id: z.union([z.string(), z.number()]),
45
+ name: z.string(),
46
+ variant: z.string().optional(),
47
+ quantity: z.number(),
48
+ lineTotal: moneySchema.optional(),
49
+ price: z.number().optional(),
50
+ image: z.string().optional(),
51
+ })
52
+
53
+ // Mirrors CheckoutForm1Totals (backend-authoritative).
54
+ export const checkoutTotalsSchema = z.object({
55
+ subtotal: moneySchema,
56
+ discount: moneySchema.optional(),
57
+ tax: moneySchema.optional(),
58
+ shipping: moneySchema.optional(),
59
+ total: moneySchema,
60
+ })
61
+
62
+ export const orderSummarySchema = z.object({
63
+ items: z.array(orderSummaryItemSchema),
64
+ totals: checkoutTotalsSchema.optional(),
65
+ shipping: z.number().optional(),
66
+ tax: z.number().optional(),
67
+ discount: z.number().optional(),
68
+ promoDiscount: z.number().optional(),
69
+ })
70
+
71
+ export const countryOptionSchema = z.object({
72
+ value: z.string(),
73
+ label: z.string(),
74
+ })
75
+
76
+ // Mirrors CheckoutForm1ShippingOption (backend shipping method id).
77
+ export const shippingOptionSchema = z.object({
78
+ id: z.string(),
79
+ name: z.string(),
80
+ price: moneySchema.optional(),
81
+ description: z.string().optional(),
82
+ estimatedDays: z.string().optional(),
83
+ })
84
+
85
+ // Mirrors CheckoutFieldErrors: per-field structured messages.
86
+ export const checkoutFieldErrorsSchema = z.record(
87
+ z.string(),
88
+ z.array(z.object({ message: z.string().optional() })),
89
+ )
90
+
91
+ // Mirrors the PaymentMethod contract from @thorprovider/types.
92
+ export const paymentMethodSchema = z.object({
93
+ id: z.string(),
94
+ name: z.string(),
95
+ type: z.enum([
96
+ 'card',
97
+ 'wallet',
98
+ 'bank_transfer',
99
+ 'buy_now_pay_later',
100
+ 'cash_on_delivery',
101
+ 'crypto',
102
+ 'other',
103
+ ]),
104
+ isEnabled: z.boolean(),
105
+ requiresSetup: z.boolean(),
106
+ description: z.string().optional(),
107
+ metadata: z.record(z.string(), z.unknown()).optional(),
108
+ features: z
109
+ .object({
110
+ saveForLater: z.boolean().optional(),
111
+ installmentPayment: z.boolean().optional(),
112
+ recurring: z.boolean().optional(),
113
+ })
114
+ .optional(),
115
+ })
116
+
117
+ export const checkoutPropsSchema = z.object({
118
+ title: z.string().optional(),
119
+ description: z.string().optional(),
120
+ orderSummary: orderSummarySchema.optional(),
121
+ countries: z.array(countryOptionSchema).optional(),
122
+ shippingOptions: z.array(shippingOptionSchema).optional(),
123
+ // Nullable: the state initializes to `null` and the block prop is optional.
124
+ selectedShippingOptionId: z.string().nullable().optional(),
125
+ paymentMethods: z.array(paymentMethodSchema).optional(),
126
+ values: checkoutFormDataSchema,
127
+ fieldErrors: checkoutFieldErrorsSchema.optional(),
128
+ fieldDescriptions: z.record(z.string(), z.string()).optional(),
129
+ })
130
+
131
+ // The empty-cart surface has no props; declared so the catalog is complete.
132
+ export const checkoutEmptyPropsSchema = z.object({})
133
+
134
+ export const fulfilmentStageSchema = z.enum(['Confirmed', 'Packed', 'Shipped', 'Delivered'])
135
+
136
+ const orderAddressSchema = z.object({
137
+ name: z.string(),
138
+ lines: z.array(z.string()),
139
+ method: z.string().optional(),
140
+ })
141
+
142
+ const orderTotalsSchema = z.object({
143
+ subtotal: moneySchema.optional(),
144
+ discount: moneySchema.optional(),
145
+ shipping: moneySchema.optional(),
146
+ tax: moneySchema.optional(),
147
+ total: moneySchema.optional(),
148
+ })
149
+
150
+ // Mirrors the CheckoutConfirmation Order block contract: backend Money totals,
151
+ // optional fields rendered only when the backend supplies them (no fabricated
152
+ // estimate or stage).
153
+ export const storefrontOrderSchema = z.object({
154
+ number: z.string(),
155
+ placedAt: z.string(),
156
+ email: z.string().optional(),
157
+ currentStage: fulfilmentStageSchema.optional(),
158
+ estimatedDelivery: z.string().nullable().optional(),
159
+ trackingHref: z.string().optional(),
160
+ shipping: orderAddressSchema.optional(),
161
+ billing: orderAddressSchema.optional(),
162
+ totals: orderTotalsSchema,
163
+ })
164
+
165
+ export const orderLineSchema = z.object({
166
+ id: z.string(),
167
+ name: z.string(),
168
+ variant: z.string().optional(),
169
+ quantity: z.number(),
170
+ lineTotal: moneySchema,
171
+ unitPrice: moneySchema.optional(),
172
+ image: z.string().optional(),
173
+ })
174
+
175
+ export const fulfilmentStageDetailSchema = z.object({
176
+ stage: fulfilmentStageSchema,
177
+ summary: z.string(),
178
+ location: z.string(),
179
+ at: z.string().nullable(),
180
+ })
181
+
182
+ // The atomic receipt: the order and its backend lines arrive together.
183
+ export const receiptSchema = z.object({
184
+ order: storefrontOrderSchema,
185
+ lines: z.array(orderLineSchema),
186
+ })
187
+
188
+ // Mirrors the CheckoutConfirmation props, including every neutral label the
189
+ // host forwards. Labels are declared here so `z.object` does not strip them;
190
+ // the registry forwards them to the block instead of relying on its defaults.
191
+ export const checkoutConfirmationPropsSchema = z.object({
192
+ receipt: receiptSchema,
193
+ fulfilmentDetails: z.record(z.string(), fulfilmentStageDetailSchema).optional(),
194
+ titleLabel: z.string().optional(),
195
+ orderPlacedLabel: z.string().optional(),
196
+ orderPlacedNoEmailLabel: z.string().optional(),
197
+ itemsLabel: z.string().optional(),
198
+ quantityLabel: z.string().optional(),
199
+ subtotalLabel: z.string().optional(),
200
+ discountLabel: z.string().optional(),
201
+ shippingLabel: z.string().optional(),
202
+ taxLabel: z.string().optional(),
203
+ totalLabel: z.string().optional(),
204
+ freeLabel: z.string().optional(),
205
+ amountUnavailableLabel: z.string().optional(),
206
+ shippingAddressLabel: z.string().optional(),
207
+ billingAddressLabel: z.string().optional(),
208
+ continueShoppingLabel: z.string().optional(),
209
+ contactSupportLabel: z.string().optional(),
210
+ supportLabel: z.string().optional(),
211
+ progressLabel: z.string().optional(),
212
+ estimatedDeliveryLabel: z.string().optional(),
213
+ currentStageLabel: z.string().optional(),
214
+ inProgressLabel: z.string().optional(),
215
+ expectedByLabel: z.string().optional(),
216
+ completedLabel: z.string().optional(),
217
+ statusLabel: z.string().optional(),
218
+ locationLabel: z.string().optional(),
219
+ trackOrderLabel: z.string().optional(),
220
+ continueShoppingHref: z.string().optional(),
221
+ supportHref: z.string().optional(),
222
+ })
223
+
224
+ export type CheckoutFormData = z.infer<typeof checkoutFormDataSchema>
225
+ export type OrderSummary = z.infer<typeof orderSummarySchema>
226
+ export type CheckoutProps = z.infer<typeof checkoutPropsSchema>
227
+ export type StorefrontOrder = z.infer<typeof storefrontOrderSchema>
228
+ export type OrderLine = z.infer<typeof orderLineSchema>
229
+ export type CheckoutConfirmationProps = z.infer<typeof checkoutConfirmationPropsSchema>
@@ -0,0 +1,33 @@
1
+ {
2
+ "view": "home",
3
+ "slots": [
4
+ { "id": "home.hero", "slot": "hero", "default": "StorefrontHero1" },
5
+ { "id": "home.benefits", "slot": "benefits", "default": "Feature1" },
6
+ { "id": "home.categories", "slot": "categories", "default": "ProductCategory2" }
7
+ ],
8
+ "sections": {
9
+ "faq": {
10
+ "elementType": "Faq",
11
+ "registryModule": "../sections/faq.client",
12
+ "registryExport": "faqRegistry",
13
+ "propsModule": "./content",
14
+ "defaultProps": "{ items: [] }"
15
+ },
16
+ "cta": {
17
+ "elementType": "Cta",
18
+ "registryModule": "../sections/cta.client",
19
+ "registryExport": "ctaRegistry",
20
+ "propsModule": "./content",
21
+ "defaultProps": "{}"
22
+ },
23
+ "products": {
24
+ "elementType": "Products",
25
+ "registryModule": "../sections/products.client",
26
+ "registryExport": "productsRegistry",
27
+ "propsModule": "./content",
28
+ "defaultProps": "{}",
29
+ "dataBound": true,
30
+ "defaultVariant": "ProductCard1"
31
+ }
32
+ }
33
+ }