@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,197 @@
1
+ 'use client'
2
+
3
+ // Generated by Thor Commerce storefront (composition core: handlers)
4
+ // Core json-render action handlers, independent of any feature. Installed
5
+ // features register their own handlers; `handlers.ts` (the composer) calls both.
6
+ //
7
+ // Cart handlers call the Server Actions (never mutate the cart locally) and
8
+ // write the backend-authoritative snapshot back into state. Payloads the blocks
9
+ // emit are preserved: adds carry the selected backend variant id and quantity,
10
+ // cart updates/removals carry the backend line-item id, and the Navbar delta is
11
+ // normalized against the authoritative line in the action.
12
+
13
+ import { useEffect } from 'react'
14
+ import { useActions, useStateStore } from '@json-render/react'
15
+ import { useNextApp } from '@json-render/next'
16
+ import {
17
+ addToCartAction,
18
+ addToCartByProductAction,
19
+ applyQuantityDeltaAction,
20
+ ensureCartAction,
21
+ loadCartStateAction,
22
+ removeLineAction,
23
+ setLineQuantityAction,
24
+ } from './actions'
25
+ import type { CartStatePatch } from './commerce'
26
+ import { cartViewAvailable } from './composition'
27
+ import { recordConfirmedTransition } from './section-observation'
28
+
29
+ /** A product-like object the views leave in `/lastClicked` (card/hero shortcut). */
30
+ interface ClickedProduct {
31
+ id?: string | number
32
+ }
33
+
34
+ /** The explicit add-to-cart payload the product overview emits. */
35
+ interface AddToCartPayload {
36
+ variantId: string
37
+ quantity: number
38
+ }
39
+
40
+ export function useCoreHandlers() {
41
+ const { get, set } = useStateStore()
42
+ const { navigate } = useNextApp()
43
+ const { registerHandler } = useActions()
44
+
45
+ useEffect(() => {
46
+ const applyCart = (patch: CartStatePatch) => {
47
+ set('/cart', patch.cart)
48
+ set('/cartTotals', patch.cartTotals)
49
+ set('/cartId', patch.cartId)
50
+ set('/cartQuantity', patch.cartQuantity)
51
+ set('/cartCompleted', patch.cartCompleted)
52
+ set('/cartSelectedShippingOptionId', patch.cartSelectedShippingOptionId)
53
+ set('/checkoutSelectedShippingOptionId', patch.cartSelectedShippingOptionId)
54
+ set('/cartError', null)
55
+ set('/cartReadError', null)
56
+ set('/cartPending', false)
57
+ }
58
+ const fail = (error: string) => {
59
+ set('/cartError', error)
60
+ set('/cartPending', false)
61
+ }
62
+ const begin = () => {
63
+ set('/cartPending', true)
64
+ set('/cartError', null)
65
+ set('/cartNotice', null)
66
+ }
67
+ // A rejected Server Action (network/RPC) must clear pending and surface a
68
+ // message; never swallow it. Only a non-secret context line is logged.
69
+ const onError = (scope: string) => (error: unknown) => {
70
+ console.error(
71
+ `[storefront/handlers] ${scope} failed`,
72
+ error instanceof Error ? error.message : error,
73
+ )
74
+ fail('No se pudo completar la operación. Inténtalo de nuevo.')
75
+ }
76
+
77
+ const handlers: Record<string, () => void> = {
78
+ addToCart: () => {
79
+ const payload = get('/lastAddToCart') as AddToCartPayload | null
80
+ if (!payload?.variantId) return
81
+ begin()
82
+ void addToCartAction({ variantId: payload.variantId, quantity: payload.quantity })
83
+ .then((result) => {
84
+ if (result.ok) {
85
+ applyCart(result.cart)
86
+ set('/cartOpen', true)
87
+ // A confirmed cart transition — observed only after the mutation
88
+ // succeeded, and only when a section origin exists (default: off).
89
+ recordConfirmedTransition('cart_add_confirmed')
90
+ } else fail(result.error)
91
+ })
92
+ .catch(onError('addToCart'))
93
+ },
94
+ addClickedToCart: () => {
95
+ // Product-card shortcut: add the clicked product's first purchasable
96
+ // variant. The backend variant id is resolved server-side from the
97
+ // product id (the card carries no variant ids).
98
+ const product = get('/lastClicked') as ClickedProduct | null
99
+ if (!product?.id) return
100
+ begin()
101
+ void addToCartByProductAction({ productId: String(product.id) })
102
+ .then((result) => {
103
+ if (result.ok) {
104
+ applyCart(result.cart)
105
+ set('/cartOpen', true)
106
+ recordConfirmedTransition('cart_add_confirmed')
107
+ } else fail(result.error)
108
+ })
109
+ .catch(onError('addClickedToCart'))
110
+ },
111
+ ensureCart: () => {
112
+ begin()
113
+ void ensureCartAction()
114
+ .then((result) => {
115
+ if (result.ok) applyCart(result.cart)
116
+ else fail(result.error)
117
+ })
118
+ .catch(onError('ensureCart'))
119
+ },
120
+ refreshCart: () => {
121
+ void loadCartStateAction().then(applyCart).catch(onError('refreshCart'))
122
+ },
123
+ updateQuantity: () => {
124
+ const { id, merchandiseId, quantity } = (get('/qtyUpdate') ?? {}) as {
125
+ id?: string
126
+ merchandiseId?: string
127
+ quantity?: number
128
+ }
129
+ if (!id || !merchandiseId || quantity == null) return
130
+ begin()
131
+ void setLineQuantityAction({ lineId: id, merchandiseId, quantity })
132
+ .then((result) => {
133
+ if (result.ok) applyCart(result.cart)
134
+ else fail(result.error)
135
+ })
136
+ .catch(onError('updateQuantity'))
137
+ },
138
+ applyQuantityDelta: () => {
139
+ const { id, delta } = (get('/qtyDelta') ?? {}) as { id?: string; delta?: number }
140
+ if (!id || delta == null) return
141
+ begin()
142
+ void applyQuantityDeltaAction({ lineId: id, delta })
143
+ .then((result) => {
144
+ if (result.ok) applyCart(result.cart)
145
+ else fail(result.error)
146
+ })
147
+ .catch(onError('applyQuantityDelta'))
148
+ },
149
+ removeFromCart: () => {
150
+ const id = get('/lastRemoved') as string | null
151
+ if (!id) return
152
+ begin()
153
+ void removeLineAction({ lineId: id })
154
+ .then((result) => {
155
+ if (result.ok) applyCart(result.cart)
156
+ else fail(result.error)
157
+ })
158
+ .catch(onError('removeFromCart'))
159
+ },
160
+ openCart: () => set('/cartOpen', true),
161
+ closeCart: () => set('/cartOpen', false),
162
+ dismissBanner: () => set('/bannerDismissed', true),
163
+ dismissDataNotice: () => set('/dataNoticeDismissed', true),
164
+ openProduct: () => {
165
+ const product = get('/lastClicked') as ClickedProduct | null
166
+ if (!product?.id) return
167
+ navigate(`/products/${product.id}`)
168
+ },
169
+ openCategory: () => {
170
+ // `/lastCategory` already holds the category href the card was given
171
+ // (`StoreCategory.href`, e.g. `/categories/jackets`); navigate it as-is
172
+ // so the route scope is not prefixed twice.
173
+ const href = get('/lastCategory') as string | null
174
+ navigate(href ?? '/categories')
175
+ },
176
+ goHome: () => {
177
+ set('/cartOpen', false)
178
+ navigate('/')
179
+ },
180
+ goToCart: () => {
181
+ // Visibility only: dismiss the lateral and go to the review route. The
182
+ // navigation is guarded by the installed composition so a store without
183
+ // the cart view never routes to a missing page.
184
+ set('/cartOpen', false)
185
+ if (cartViewAvailable) navigate('/cart')
186
+ },
187
+ submitReview: () => {},
188
+ toggleReviewLike: () => {},
189
+ }
190
+
191
+ for (const [name, handler] of Object.entries(handlers)) {
192
+ registerHandler(name, handler)
193
+ }
194
+ // registerHandler uses functional state updates; store/navigate captured once.
195
+ // eslint-disable-next-line react-hooks/exhaustive-deps
196
+ }, [])
197
+ }
@@ -1,42 +1,11 @@
1
1
  'use client'
2
2
 
3
- // Generated by Thor Commerce storefront (template: assets/templates/app/lib/__STOREFRONT__/handlers.ts)
4
- // Registers json-render action handlers. Recipe-added views extend this file.
3
+ // Generated by Thor Commerce storefront (composition composer: handlers).
4
+ // Do not edit by hand — the manifest is the source of truth; run `storefront sync`.
5
+ // Installed features contribute their handlers below.
5
6
 
6
- import { useEffect } from 'react'
7
- import { useActions, useStateStore } from '@json-render/react'
8
- import { useNextApp } from '@json-render/next'
7
+ import { useCoreHandlers } from './handlers.core'
9
8
 
10
9
  export function useStorefrontHandlers() {
11
- const { set } = useStateStore()
12
- const { navigate } = useNextApp()
13
- const { registerHandler } = useActions()
14
-
15
- useEffect(() => {
16
- const handlers: Record<string, () => void> = {
17
- addToCart: () => {},
18
- openCart: () => {},
19
- openProduct: () => navigate('/productos'),
20
- openCategory: () => {
21
- const handle = ''
22
- navigate(handle ? `/categorias/${handle}` : '/categorias')
23
- },
24
- goHome: () => navigate('/'),
25
- applyFilters: () => {},
26
- placeOrder: () => navigate('/checkout'),
27
- continueShopping: () => navigate('/'),
28
- viewOrder: () => {},
29
- downloadInvoice: () => {},
30
- submitReview: () => {},
31
- toggleReviewLike: () => {},
32
- newsletterSubmit: () => {},
33
- }
34
-
35
- for (const [name, handler] of Object.entries(handlers)) {
36
- registerHandler(name, handler)
37
- }
38
- void set
39
- // registerHandler uses functional state updates; store/navigate captured once.
40
- // eslint-disable-next-line react-hooks/exhaustive-deps
41
- }, [])
10
+ useCoreHandlers()
42
11
  }
@@ -0,0 +1,27 @@
1
+ // Generated by Thor Commerce storefront (template: assets/templates/app/lib/__STOREFRONT__/icon-map.ts)
2
+ // Static icon-name → lucide component map. View specs reference icons by
3
+ // kebab-case name (they are data, serialized through state/spec); view
4
+ // registries resolve them here so blocks receive real components — never raw
5
+ // strings, which React renders as unknown HTML tags. Extend this map when a
6
+ // view introduces a new icon name.
7
+
8
+ import type { LucideIcon } from 'lucide-react'
9
+ import { Circle, Gift, Headphones, Percent, ShieldCheck, Star, Truck, Zap } from 'lucide-react'
10
+
11
+ /** Neutral fallback rendered for names not present in the map. */
12
+ export const FALLBACK_ICON: LucideIcon = Circle
13
+
14
+ export const iconMap: Record<string, LucideIcon> = {
15
+ gift: Gift,
16
+ headphones: Headphones,
17
+ percent: Percent,
18
+ 'shield-check': ShieldCheck,
19
+ star: Star,
20
+ truck: Truck,
21
+ zap: Zap,
22
+ }
23
+
24
+ /** Resolve a spec icon name to a lucide component, falling back safely. */
25
+ export function resolveIcon(name: string | undefined): LucideIcon {
26
+ return (name && iconMap[name]) || FALLBACK_ICON
27
+ }
@@ -3,8 +3,7 @@
3
3
  // Kept out of the views so a view module stays a pure server spec; mappers are
4
4
  // data-only and imported where the loader builds block props.
5
5
 
6
- import type { CardProduct, FeaturedProduct, ProductCategory2Category } from '@thorprovider/blocks'
7
- import type { ProductCategory } from '@thorprovider/types'
6
+ import type { CardProduct, FeaturedProduct, ProductCategory2Category, StoreCategory } from '@thorprovider/blocks'
8
7
 
9
8
  export const CATEGORY_IMAGE_FALLBACK =
10
9
  "data:image/svg+xml;charset=utf-8," +
@@ -32,16 +31,19 @@ export function toFeaturedProduct(card: CardProduct): FeaturedProduct {
32
31
  }
33
32
  }
34
33
 
35
- /** ProductCategory (types canon) → ProductCategory2Category (block), with
36
- * fallbacks for description, image, and itemCount the backend may omit. */
37
- export function toCategoryCard(category: ProductCategory): ProductCategory2Category {
34
+ /** StoreCategory (what @thorprovider/runtime/commerce's getCatalog returns) →
35
+ * ProductCategory2Category (block), with fallbacks for description and image.
36
+ * The runtime projection keeps only label/href and carries no product count, so
37
+ * `itemCount` is unknown here: it is OMITTED (the block then renders no count)
38
+ * rather than fabricated as `0`. A genuine `0` supplied by a richer source is
39
+ * preserved because `itemCount` is optional, not defaulted. */
40
+ export function toCategoryCard(category: StoreCategory): ProductCategory2Category {
38
41
  return {
39
- id: category.id,
40
- handle: category.handle,
41
- name: category.name,
42
- description: category.description ?? '',
43
- itemCount: category.productCount ?? 0,
44
- image: category.image ?? category.metadata?.image ?? CATEGORY_IMAGE_FALLBACK,
42
+ id: category.label,
43
+ handle: category.href,
44
+ name: category.label,
45
+ description: '',
46
+ image: CATEGORY_IMAGE_FALLBACK,
45
47
  trending: false,
46
48
  }
47
49
  }
@@ -2,31 +2,250 @@
2
2
 
3
3
  // Generated by Thor Commerce storefront (template: assets/templates/app/lib/__STOREFRONT__/registry.tsx)
4
4
  // Base registry for the app shell plus every registered view's registry fragment.
5
+ //
6
+ // The shell renders the live backend cart: the Navbar mini-cart and the cart
7
+ // drawer read the authoritative cart state and dispatch cart mutations to the
8
+ // Server Actions. A listing card's add-to-cart adds the product's first
9
+ // purchasable variant (the card shortcut); opening a product from a listing
10
+ // navigates to the detail, where a variant is chosen explicitly.
5
11
 
6
- import type { Components } from '@json-render/react'
7
12
  import { defineRegistry, useStateStore } from '@json-render/react'
8
- import { Navbar1, ProductCard1 } from '@thorprovider/blocks'
13
+ import { useEffect } from 'react'
14
+ import { usePathname } from 'next/navigation'
15
+ import {
16
+ BackToTop as BackToTopBlock,
17
+ Banner1,
18
+ FooterSection1,
19
+ MobileBottomNav as MobileBottomNavBlock,
20
+ Navbar1,
21
+ Navbar4,
22
+ ProductCard1,
23
+ SkipToContent as SkipToContentBlock,
24
+ type BackToTopProps,
25
+ type MobileBottomNavProps,
26
+ type SkipToContentProps,
27
+ } from '@thorprovider/blocks'
28
+ import { isAvailableMoney } from '@thorprovider/blocks/contracts'
29
+ import type { Money } from '@thorprovider/types'
9
30
  import { catalog } from './catalog'
31
+ import type { CartLineState, CartTotalsState } from './state'
10
32
  import { useStorefrontHandlers } from './handlers'
11
- import { views } from './views'
33
+ import { cartViewAvailable, checkoutAvailable, mountedFeatureNames } from './composition'
34
+ import {
35
+ bannerPropsSchema,
36
+ footerPropsSchema,
37
+ navbarPropsSchema,
38
+ productCardPropsSchema,
39
+ storefrontNavbarPropsSchema,
40
+ } from './schema'
41
+ import { viewRegistries } from './views/registry'
42
+
43
+ // Navbar4 accepts its own mini-cart line shape (`qty`), distinct from the block
44
+ // `CartItem` contract (`quantity`). Types are local so the asymmetry is explicit.
45
+ interface NavCartItem {
46
+ id: string
47
+ name: string
48
+ variant?: string
49
+ price: number
50
+ qty: number
51
+ image?: string
52
+ lineTotal?: Money
53
+ }
54
+
55
+ /** CartLineState -> Navbar4 cart line. `price` is legacy; the authoritative
56
+ * subtotal and per-line total are passed separately, so neither is computed
57
+ * from the line. */
58
+ function toNavCartItems(cart: CartLineState[]): NavCartItem[] {
59
+ return cart.map((line) => ({
60
+ id: line.id,
61
+ name: line.name,
62
+ variant: line.variant,
63
+ price: 0,
64
+ qty: line.quantity,
65
+ image: line.image,
66
+ lineTotal: line.lineTotal,
67
+ }))
68
+ }
12
69
 
13
70
  const baseComponents = {
14
- Layout: ({ children }: { children?: React.ReactNode }) => {
71
+ Layout: ({ children, slots }: { children?: React.ReactNode; slots?: Record<string, React.ReactNode> }) => {
15
72
  useStorefrontHandlers()
16
- return <div className="flex min-h-screen flex-col">{children}</div>
73
+ const { set } = useStateStore()
74
+ const pathname = usePathname()
75
+ // The mobile bottom navigation is feature-gated; when it is installed the
76
+ // main content reserves bottom padding so the fixed bar never obscures it.
77
+ const hasBottomNav = mountedFeatureNames.includes('navigability')
78
+ // Central dismissal: any route or history change closes the shared cart
79
+ // lateral, so it never lingers across navigation (forward/back included).
80
+ useEffect(() => {
81
+ set('/cartOpen', false)
82
+ }, [pathname, set])
83
+ return (
84
+ <div className="flex min-h-screen flex-col">
85
+ {slots?.chrome}
86
+ {slots?.notices}
87
+ <header className="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
88
+ {slots?.header}
89
+ </header>
90
+ <main
91
+ id="main-content"
92
+ className={hasBottomNav ? 'flex-1 pb-16 md:pb-0' : 'flex-1'}
93
+ >
94
+ {children}
95
+ </main>
96
+ <footer>{slots?.footer}</footer>
97
+ </div>
98
+ )
99
+ },
100
+ // Neutral page root: renders the route page without shell chrome. Using it
101
+ // instead of `Layout` keeps the shell from mounting twice per page.
102
+ Page: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
103
+ // Navigability chrome, feature-gated by the shell spec (only referenced when
104
+ // the feature is installed). Props are cast to the block contracts; the
105
+ // catalog schema is the runtime guard.
106
+ SkipToContent: ({ props }: { props: Record<string, unknown> }) => {
107
+ const p = props as unknown as SkipToContentProps
108
+ return <SkipToContentBlock {...p} />
109
+ },
110
+ BackToTop: ({ props }: { props: Record<string, unknown> }) => {
111
+ const p = props as unknown as BackToTopProps
112
+ return <BackToTopBlock {...p} />
113
+ },
114
+ MobileBottomNav: ({ props }: { props: Record<string, unknown> }) => {
115
+ const pathname = usePathname()
116
+ const p = props as unknown as MobileBottomNavProps
117
+ return <MobileBottomNavBlock {...p} currentHref={p.currentHref ?? pathname} />
118
+ },
119
+ // Reserved top region for system/status messages. Self-gates on state and
120
+ // collapses to nothing when there is no notice. A configured mock is labelled
121
+ // as demo data; a real-mode failure is surfaced visibly, never hidden.
122
+ SystemNotice: () => {
123
+ const { state, set } = useStateStore()
124
+ if (state.dataNoticeDismissed) return null
125
+ const cartError = typeof state.cartReadError === 'string' ? state.cartReadError : null
126
+ const error = cartError ?? (typeof state.dataError === 'string' ? state.dataError : null)
127
+ const isMock = state.dataSource === 'mock'
128
+ if (!error && !isMock) return null
129
+ const destructive = Boolean(error)
130
+ return (
131
+ <div
132
+ role="alert"
133
+ className={
134
+ destructive
135
+ ? 'flex items-center justify-between gap-4 border-b border-destructive/40 bg-destructive/10 px-4 py-2 text-sm text-destructive'
136
+ : 'flex items-center justify-between gap-4 border-b border-border bg-muted px-4 py-2 text-sm text-muted-foreground'
137
+ }
138
+ >
139
+ <span>
140
+ {error ??
141
+ 'Datos de demostración (mock): no hay un backend real configurado.'}
142
+ </span>
143
+ <button
144
+ type="button"
145
+ aria-label="Cerrar aviso"
146
+ onClick={() => set('/dataNoticeDismissed', true)}
147
+ className="shrink-0 rounded p-1 hover:bg-destructive/20"
148
+ >
149
+ X
150
+ </button>
151
+ </div>
152
+ )
153
+ },
154
+ StorefrontNavbar: ({ props, emit }: { props: Record<string, unknown>; emit: (event: string) => void }) => {
155
+ const parsed = storefrontNavbarPropsSchema.parse(props)
156
+ const { state, set } = useStateStore()
157
+ const cart = (state.cart ?? []) as CartLineState[]
158
+ const totals = state.cartTotals as CartTotalsState | null
159
+ const cartError = typeof state.cartError === 'string' ? state.cartError : undefined
160
+ const readError = typeof state.cartReadError === 'string' ? state.cartReadError : undefined
161
+ const errorLabel = cartError ?? readError
162
+ const cartState = state.cartPending ? 'pending' : errorLabel ? 'error' : 'idle'
163
+ // Checkout is offered only when the required backend amounts are present and
164
+ // valid: a missing/malformed subtotal, total, or line amount blocks it, while
165
+ // an undetermined shipping amount does not (AC-CART-4.4 / UC-CART-7).
166
+ const amountsValid =
167
+ cart.every((line) => isAvailableMoney(line.lineTotal)) &&
168
+ totals !== null &&
169
+ isAvailableMoney(totals.subtotal) &&
170
+ isAvailableMoney(totals.total)
171
+ return (
172
+ <Navbar4
173
+ storeCategories={parsed.storeCategories}
174
+ shippingNote={parsed.shippingNote}
175
+ utilityLinks={parsed.utilityLinks}
176
+ logoText={parsed.logoText}
177
+ logoHref={parsed.logoHref}
178
+ logoAccent={parsed.logoAccent}
179
+ className={parsed.className}
180
+ accountHref={parsed.accountHref}
181
+ // The current route marks the matching category link as the current page
182
+ // (accessibly, beyond color alone); no link is marked when it does not
183
+ // match any destination.
184
+ currentPath={usePathname()}
185
+ // Held global search (E2 task 5.4): default to hidden so no global
186
+ // search is presented while the contract is unverified; the user-owned
187
+ // shell seam may opt in with `showSearch: true`.
188
+ showSearch={parsed.showSearch ?? false}
189
+ items={toNavCartItems(cart)}
190
+ subtotal={totals?.subtotal}
191
+ // One shared lateral: visibility is driven by the shared `cartOpen`
192
+ // state (a successful add opens it; navigation dismisses it). The
193
+ // canonical line amounts and lifecycle states come from the same cart
194
+ // state the standalone `/cart` view reads.
195
+ canonicalCart
196
+ cartState={cartState}
197
+ cartErrorLabel={errorLabel}
198
+ onCartRetry={() => emit('refreshCart')}
199
+ cartOpen={state.cartOpen === true}
200
+ onCartOpenChange={(open) => set('/cartOpen', open)}
201
+ checkoutUnavailable={!checkoutAvailable}
202
+ checkoutDisabled={cartState !== 'idle' || cart.length === 0 || !amountsValid}
203
+ viewCartUnavailable={!cartViewAvailable}
204
+ onViewCart={() => emit('goToCart')}
205
+ onCheckout={() => emit('goToCheckout')}
206
+ onQtyChange={(id, delta) => {
207
+ set('/qtyDelta', { id, delta })
208
+ emit('applyQuantityDelta')
209
+ }}
210
+ onRemove={(id) => {
211
+ set('/lastRemoved', id)
212
+ emit('removeFromCart')
213
+ }}
214
+ />
215
+ )
216
+ },
217
+ Banner: ({ props, emit }: { props: Record<string, unknown>; emit: (event: string) => void }) => {
218
+ const parsed = bannerPropsSchema.parse(props)
219
+ const { set } = useStateStore()
220
+ return (
221
+ <Banner1
222
+ badge={parsed.badge}
223
+ message={parsed.message}
224
+ href={parsed.href}
225
+ linkLabel={parsed.linkLabel}
226
+ avatars={parsed.avatars}
227
+ onDismiss={() => {
228
+ set('/bannerDismissed', true)
229
+ emit('dismissBanner')
230
+ }}
231
+ />
232
+ )
233
+ },
234
+ Navbar: ({ props, emit }: { props: Record<string, unknown>; emit: (event: string) => void }) => {
235
+ const parsed = navbarPropsSchema.parse(props)
236
+ return (
237
+ <Navbar1
238
+ navLinks={parsed.navLinks}
239
+ logoText={parsed.logoText}
240
+ logoHref={parsed.logoHref}
241
+ currentPath={parsed.currentPath}
242
+ signIn={parsed.signIn}
243
+ cta={parsed.cta}
244
+ cartCount={parsed.cartCount}
245
+ onOpenCart={() => emit('openCart')}
246
+ />
247
+ )
17
248
  },
18
- Navbar: ({ props, emit }: { props: Record<string, unknown>; emit: (event: string) => void }) => (
19
- <Navbar1
20
- navLinks={props.navLinks as never}
21
- logoText={props.logoText as string}
22
- logoHref={props.logoHref as string}
23
- currentPath={props.currentPath as string}
24
- signIn={props.signIn as never}
25
- cta={props.cta as never}
26
- cartCount={props.cartCount as number}
27
- onOpenCart={() => emit('openCart')}
28
- />
29
- ),
30
249
  ProductCard: ({
31
250
  props,
32
251
  emit,
@@ -34,18 +253,20 @@ const baseComponents = {
34
253
  props: Record<string, unknown>
35
254
  emit: (event: string) => void
36
255
  }) => {
256
+ const parsed = productCardPropsSchema.parse(props)
37
257
  const { set } = useStateStore()
38
258
  const select = (product: unknown) => {
39
259
  set('/lastClicked', product)
40
260
  }
41
261
  return (
42
262
  <ProductCard1
43
- products={props.products as never}
44
- heading={props.heading as string}
45
- maxItems={props.maxItems as number}
263
+ products={parsed.products}
264
+ heading={parsed.heading}
265
+ maxItems={parsed.maxItems}
266
+ // This base card opens the detail so a variant is chosen explicitly.
46
267
  onAddToCart={(product) => {
47
268
  select(product)
48
- emit('addToCart')
269
+ emit('openProduct')
49
270
  }}
50
271
  onProductClick={(product) => {
51
272
  select(product)
@@ -55,44 +276,36 @@ const baseComponents = {
55
276
  )
56
277
  },
57
278
  Footer: ({ props }: { props: Record<string, unknown> }) => {
58
- const brandName = props.brandName as string
59
- const brandDescription = props.brandDescription as string | undefined
60
- const linkColumns =
61
- (props.linkColumns as { title: string; links: { label: string; href?: string }[] }[]) ?? []
279
+ const parsed = footerPropsSchema.parse(props)
62
280
  return (
63
- <footer className="mt-auto border-t bg-background">
64
- <div className="mx-auto grid max-w-6xl gap-8 px-4 py-12 sm:grid-cols-2 lg:grid-cols-4">
65
- <div className="space-y-2">
66
- <p className="text-lg font-semibold">{brandName}</p>
67
- {brandDescription ? (
68
- <p className="text-sm text-muted-foreground">{brandDescription}</p>
69
- ) : null}
70
- </div>
71
- {linkColumns.map((column) => (
72
- <div key={column.title} className="space-y-2">
73
- <p className="text-sm font-medium">{column.title}</p>
74
- <ul className="space-y-1">
75
- {column.links.map((link) => (
76
- <li key={link.label}>
77
- <a className="text-sm text-muted-foreground hover:text-foreground" href={link.href ?? '#'}>
78
- {link.label}
79
- </a>
80
- </li>
81
- ))}
82
- </ul>
83
- </div>
84
- ))}
85
- </div>
86
- </footer>
281
+ <FooterSection1
282
+ brandName={parsed.brandName}
283
+ brandAccent={parsed.brandAccent}
284
+ brandDescription={parsed.brandDescription}
285
+ linkColumns={parsed.linkColumns}
286
+ className={parsed.className}
287
+ // The scaffold ships no newsletter submit callback. The seam's
288
+ // `newsletter` is content only, so the block swallows it and renders no
289
+ // form. Wiring a real host callback (which would enable the form) is an
290
+ // external integration, out of scope for the scaffold.
291
+ newsletter={
292
+ parsed.newsletter
293
+ ? {
294
+ placeholder: parsed.newsletter.placeholder,
295
+ buttonLabel: parsed.newsletter.buttonLabel ?? 'Suscribirme',
296
+ }
297
+ : undefined
298
+ }
299
+ />
87
300
  )
88
301
  },
89
302
  }
90
303
 
91
- const viewComponents = Object.assign({}, ...views.map((view) => view.registry))
304
+ const viewComponents = Object.assign({}, ...viewRegistries)
92
305
 
93
306
  export const { registry } = defineRegistry(catalog, {
94
307
  components: {
95
308
  ...baseComponents,
96
309
  ...viewComponents,
97
- } as unknown as Components<typeof catalog>,
310
+ },
98
311
  })