@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,90 @@
1
+ // Thor Commerce view module: home
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 -> "home".
6
+
7
+ import { getCatalog } from '@thorprovider/runtime/commerce'
8
+ import { getDataSource, getStoreContext } from '../../store-context'
9
+ import { pageRoot } from '../../spec/page-root'
10
+ import type { StorefrontElement } from '../../spec/types'
11
+ import type { ViewModule } from '../types'
12
+ import { registry } from './registry'
13
+ import { benefitsProps, categoriesProps, heroProps } from './props'
14
+ import * as homeData from './data'
15
+ import { toCategoryCard, toFeaturedProduct } from '../../mappers'
16
+ import { resolveSectionSources, resolveSectionViewStates } from '../../section-sources'
17
+ import {
18
+ categoryCardsPropsSchema,
19
+ featurePropsSchema,
20
+ storefrontHeroPropsSchema,
21
+ } from './schema'@@SECTION_IMPORTS@@
22
+
23
+ // The element tree is derived wiring; the rendered props come from the
24
+ // user-owned ./props.ts module so they survive sync and unit removal.
25
+ const elements: Record<string, StorefrontElement> = {
26
+ @@ELEMENTS@@
27
+ }
28
+
29
+ const catalog = {
30
+ @@CATALOG@@
31
+ }
32
+
33
+ export const home: ViewModule = {
34
+ name: 'home',
35
+ elements,
36
+ catalog,
37
+ registry,
38
+ route: {
39
+ path: '/',
40
+ loaderName: 'home',
41
+ layout: 'default',
42
+ metadata: {
43
+ title: 'Inicio',
44
+ description: 'Descubre los productos de __SITE_NAME__.',
45
+ },
46
+ entry: pageRoot('home', [@@SLOT_IDS@@], elements),
47
+ },
48
+ loader: {
49
+ home: async () => {
50
+ // Data-bound sections resolve independently through the canonical catalog
51
+ // bridge; equal queries share request-local work and differing contexts
52
+ // stay isolated. A section failure never erases its siblings, and it is
53
+ // resolved separately so a catalog failure still yields truthful
54
+ // per-section state instead of a missing one.
55
+ let sections: Record<string, unknown> = {}
56
+ try {
57
+ sections = await resolveSectionViewStates(
58
+ @@SECTION_LOADERS@@,
59
+ resolveSectionSources(homeData),
60
+ getStoreContext(),
61
+ )
62
+ } catch {
63
+ sections = {}
64
+ }
65
+ try {
66
+ const { products, categories, source } = await getCatalog()
67
+ return {
68
+ heroProducts: homeData.selectHeroProducts(products).map(toFeaturedProduct),
69
+ categories: categories.map(toCategoryCard),
70
+ navCategories: homeData.toNavCategories(categories),
71
+ dataSource: source,
72
+ dataError: null,
73
+ sections,
74
+ }
75
+ } catch (error) {
76
+ // A real-mode failure is surfaced as a visible error, never as fictional
77
+ // data. Mock mode is labelled by `dataSource`.
78
+ return {
79
+ heroProducts: [],
80
+ categories: [],
81
+ navCategories: [],
82
+ dataSource: getDataSource(),
83
+ dataError:
84
+ error instanceof Error ? error.message : 'No se pudo cargar el catálogo.',
85
+ sections,
86
+ }
87
+ }
88
+ },
89
+ },
90
+ }
@@ -0,0 +1,25 @@
1
+ 'use client'
2
+
3
+ // Thor Commerce view registry fragment: home
4
+ // Client boundary for the home 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
+ // Props arrive as untyped spec data: every entry parses them through the view's
10
+ // schema (./schema.ts) before binding, so a contract mismatch fails loudly at
11
+ // the boundary instead of rendering undefined fields or literal icon tags.
12
+
13
+ @@BLOCK_IMPORTS@@@@SECTION_REGISTRY_IMPORTS@@
14
+ import { useStateStore } from '@json-render/react'
15
+ import { resolveIcon } from '../../icon-map'
16
+ import type { ViewRegistry } from '../types'
17
+ import {
18
+ categoryCardsPropsSchema,
19
+ featurePropsSchema,
20
+ storefrontHeroPropsSchema,
21
+ } from './schema'
22
+
23
+ export const registry: ViewRegistry = {
24
+ @@REGISTRY@@
25
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "module": "@thorprovider/blocks",
3
+ "import": "Feature1",
4
+ "as": "FeatureBlock"
5
+ }
@@ -0,0 +1,4 @@
1
+ Feature: {
2
+ props: featurePropsSchema,
3
+ description: 'Benefit highlights.',
4
+ },
@@ -0,0 +1,4 @@
1
+ 'home.benefits': {
2
+ type: 'Feature',
3
+ props: benefitsProps,
4
+ },
@@ -0,0 +1,11 @@
1
+ Feature: ({ props }) => {
2
+ const feature = featurePropsSchema.parse(props)
3
+ return (
4
+ <FeatureBlock
5
+ items={feature.items.map((item) => ({ ...item, icon: resolveIcon(item.icon) }))}
6
+ badge={feature.badge}
7
+ title={feature.title}
8
+ description={feature.description}
9
+ />
10
+ )
11
+ },
@@ -0,0 +1,5 @@
1
+ {
2
+ "module": "@thorprovider/blocks",
3
+ "import": "Feature2",
4
+ "as": "FeatureBlock"
5
+ }
@@ -0,0 +1,4 @@
1
+ Feature: {
2
+ props: featurePropsSchema,
3
+ description: 'Benefit highlights.',
4
+ },
@@ -0,0 +1,4 @@
1
+ 'home.benefits': {
2
+ type: 'Feature',
3
+ props: benefitsProps,
4
+ },
@@ -0,0 +1,10 @@
1
+ Feature: ({ props }) => {
2
+ const feature = featurePropsSchema.parse(props)
3
+ return (
4
+ <FeatureBlock
5
+ items={feature.items.map((item) => ({ ...item, icon: resolveIcon(item.icon) }))}
6
+ title={feature.title}
7
+ description={feature.description}
8
+ />
9
+ )
10
+ },
@@ -0,0 +1,5 @@
1
+ {
2
+ "module": "@thorprovider/blocks",
3
+ "import": "ProductCategory2",
4
+ "as": "CategoryCardsBlock"
5
+ }
@@ -0,0 +1,4 @@
1
+ CategoryCards: {
2
+ props: categoryCardsPropsSchema,
3
+ description: 'Category grid.',
4
+ },
@@ -0,0 +1,5 @@
1
+ 'home.categories': {
2
+ type: 'CategoryCards',
3
+ props: categoriesProps,
4
+ on: { openCategory: { action: 'openCategory' } },
5
+ },
@@ -0,0 +1,16 @@
1
+ CategoryCards: ({ props, emit }) => {
2
+ const { set } = useStateStore()
3
+ const cards = categoryCardsPropsSchema.parse(props)
4
+ return (
5
+ <CategoryCardsBlock
6
+ title={cards.title}
7
+ description={cards.description}
8
+ categories={cards.categories}
9
+ cta={cards.cta}
10
+ onBrowse={(category) => {
11
+ set('/lastCategory', category.handle ?? String(category.id))
12
+ emit('openCategory')
13
+ }}
14
+ />
15
+ )
16
+ },
@@ -0,0 +1,5 @@
1
+ {
2
+ "module": "@thorprovider/blocks",
3
+ "import": "StorefrontHero1",
4
+ "as": "StorefrontHeroBlock"
5
+ }
@@ -0,0 +1,4 @@
1
+ StorefrontHero: {
2
+ props: storefrontHeroPropsSchema,
3
+ description: 'Landing hero.',
4
+ },
@@ -0,0 +1,5 @@
1
+ 'home.hero': {
2
+ type: 'StorefrontHero',
3
+ props: heroProps,
4
+ on: { addClickedToCart: { action: 'addClickedToCart' }, openProduct: { action: 'openProduct' } },
5
+ },
@@ -0,0 +1,26 @@
1
+ StorefrontHero: ({ props, emit }) => {
2
+ const { set } = useStateStore()
3
+ const hero = storefrontHeroPropsSchema.parse(props)
4
+ return (
5
+ <StorefrontHeroBlock
6
+ title={hero.title}
7
+ description={hero.description}
8
+ products={hero.products}
9
+ titleAccent={hero.titleAccent}
10
+ badge={hero.badge}
11
+ primaryCta={hero.primaryCta}
12
+ secondaryCta={hero.secondaryCta}
13
+ heroImage={hero.heroImage}
14
+ onAddToCart={(product) => {
15
+ // Product-card shortcut: add the first purchasable variant.
16
+ set('/lastClicked', product)
17
+ emit('addClickedToCart')
18
+ }}
19
+ onBuyNow={(product) => {
20
+ set('/lastClicked', product)
21
+ emit('openProduct')
22
+ }}
23
+ onSelectColor={() => {}}
24
+ />
25
+ )
26
+ },
@@ -0,0 +1,70 @@
1
+ // Thor Commerce view content: home
2
+ // User-owned view content — the copy, CTA labels, and feature items rendered by
3
+ // this view. Reconcile seeds this file once and never rewrites it; the derived
4
+ // view module (./index.tsx) imports it and wires it into the element tree, so
5
+ // your edits here survive `storefront sync` and unit removal. Plain data only:
6
+ // no React and no server imports.
7
+
8
+ export interface HeroImageContent {
9
+ src: string
10
+ alt: string
11
+ }
12
+
13
+ /**
14
+ * Optional editorial hero image. Leave `undefined` to render no image; never
15
+ * set a blank `src`. This is static editorial imagery only — never a product
16
+ * image or backend data.
17
+ */
18
+ export const heroImage: HeroImageContent | undefined = undefined
19
+
20
+ export const heroContent = {
21
+ badge: 'Bienvenido',
22
+ title: '__SITE_NAME__',
23
+ titleAccent: 'para cada proyecto',
24
+ // Neutral: no delivery/shipping promise.
25
+ description: 'Productos seleccionados con precios claros y compra sencilla.',
26
+ primaryCta: { label: 'Comprar ahora' },
27
+ secondaryCta: { label: 'Ver categorías' },
28
+ }
29
+
30
+ export const benefitsContent = {
31
+ badge: '__SITE_NAME__',
32
+ title: 'Compra con confianza',
33
+ // Neutral: no delivery/shipping promise.
34
+ description: 'Ventajas pensadas para una compra clara y segura.',
35
+ items: [
36
+ {
37
+ id: 'pago',
38
+ title: 'Pago protegido',
39
+ description: 'Compra con confianza, múltiples opciones de pago.',
40
+ icon: 'shield-check',
41
+ },
42
+ {
43
+ id: 'catalogo',
44
+ title: 'Catálogo variado',
45
+ description: 'Productos para tu hogar y tu negocio.',
46
+ icon: 'star',
47
+ },
48
+ {
49
+ id: 'soporte',
50
+ title: 'Soporte local',
51
+ description: 'Atención personalizada en todo el país.',
52
+ icon: 'headphones',
53
+ },
54
+ ],
55
+ }
56
+
57
+ export const categoriesContent = {
58
+ title: 'Compra por categoría',
59
+ description: 'Explora nuestro catálogo completo.',
60
+ }
61
+
62
+ /**
63
+ * Editorial section instance content, keyed by the section instance id the
64
+ * manifest persists (`views[].sections[].id`, e.g. `home-faq-1`). The derived
65
+ * view module reads `sectionContent[id]`, so adding, reordering, or removing a
66
+ * section never rewrites this file and your copy survives `storefront sync`.
67
+ * Plain data only, matching the section fragment's props; an unset id renders
68
+ * the section's neutral default.
69
+ */
70
+ export const sectionContent: Record<string, Record<string, unknown>> = {}
@@ -0,0 +1,24 @@
1
+ // Thor Commerce view data: home
2
+ // User-owned loader shaping for the home view. Reconcile seeds this file once and
3
+ // never rewrites it; the derived view module (./index.tsx) imports these helpers
4
+ // and calls them from its loader, so edits here survive `storefront sync` and
5
+ // unit removal. Pure functions only: no React and no server imports.
6
+
7
+ export function selectHeroProducts<T>(products: readonly T[]): T[] {
8
+ return products.slice(0, 4)
9
+ }
10
+
11
+ export function toNavCategories(
12
+ categories: readonly { label: string; href: string }[],
13
+ ): { label: string; href: string }[] {
14
+ return categories.map((category) => ({ label: category.label, href: category.href }))
15
+ }
16
+
17
+ // Per-instance selection for data-bound sections (for example `products`), keyed
18
+ // by the section's stable id from the composition manifest. Each value is a
19
+ // canonical catalog selection (category/collection/search/ordering) that the
20
+ // generated host loader resolves through the runtime catalog bridge. Left empty
21
+ // by default: a data-bound section with no entry renders an `unconfigured`
22
+ // state. Entries whose id is no longer referenced by the composition are kept
23
+ // but never queried, so re-adding the section with the same id restores them.
24
+ export const sectionSources: Record<string, unknown> = {}
@@ -5,115 +5,51 @@
5
5
  // on the server. Registered by importing it in ../index.ts. Recipe: recipes.json -> "home".
6
6
 
7
7
  import { getCatalog } from '@thorprovider/runtime/commerce'
8
- import { z } from 'zod'
8
+ import { getDataSource, getStoreContext } from '../../store-context'
9
9
  import { pageRoot } from '../../spec/page-root'
10
10
  import type { StorefrontElement } from '../../spec/types'
11
11
  import type { ViewModule } from '../types'
12
12
  import { registry } from './registry'
13
- import { toFeaturedProduct } from '../../mappers'
13
+ import { benefitsProps, categoriesProps, heroProps } from './props'
14
+ import * as homeData from './data'
15
+ import { toCategoryCard, toFeaturedProduct } from '../../mappers'
16
+ import { resolveSectionSources, resolveSectionViewStates } from '../../section-sources'
17
+ import {
18
+ categoryCardsPropsSchema,
19
+ featurePropsSchema,
20
+ storefrontHeroPropsSchema,
21
+ } from './schema'
14
22
 
23
+ // The element tree is derived wiring; the rendered props come from the
24
+ // user-owned ./props.ts module so they survive sync and unit removal.
15
25
  const elements: Record<string, StorefrontElement> = {
16
26
  'home.hero': {
17
27
  type: 'StorefrontHero',
18
- props: {
19
- badge: 'Bienvenido',
20
- title: '__SITE_NAME__',
21
- titleAccent: 'para cada proyecto',
22
- description: 'Productos seleccionados con precios justos y entrega a tu puerta.',
23
- products: { $state: '/heroProducts' },
24
- primaryCta: { label: 'Comprar ahora' },
25
- secondaryCta: { label: 'Ver categorías' },
26
- },
27
- on: { addToCart: { action: 'addToCart' }, openProduct: { action: 'openProduct' } },
28
+ props: heroProps,
29
+ on: { addClickedToCart: { action: 'addClickedToCart' }, openProduct: { action: 'openProduct' } },
28
30
  },
29
31
  'home.benefits': {
30
32
  type: 'Feature',
31
- props: {
32
- badge: '__SITE_NAME__',
33
- title: 'Compra con confianza',
34
- description: 'Beneficios pensados para que tu compra llegue segura y a tiempo.',
35
- items: [
36
- {
37
- id: 'entrega',
38
- title: 'Entrega segura',
39
- description: 'Seguimiento de tu pedido hasta la puerta de tu casa.',
40
- icon: 'truck',
41
- },
42
- {
43
- id: 'pago',
44
- title: 'Pago protegido',
45
- description: 'Compra con confianza, múltiples opciones de pago.',
46
- icon: 'shield-check',
47
- },
48
- {
49
- id: 'soporte',
50
- title: 'Soporte local',
51
- description: 'Atención personalizada en todo el país.',
52
- icon: 'headphones',
53
- },
54
- ],
55
- },
33
+ props: benefitsProps,
56
34
  },
57
35
  'home.categories': {
58
36
  type: 'CategoryCards',
59
- props: {
60
- title: 'Compra por categoría',
61
- description: 'Explora nuestro catálogo completo.',
62
- categories: { $state: '/categories' },
63
- },
37
+ props: categoriesProps,
64
38
  on: { openCategory: { action: 'openCategory' } },
65
39
  },
66
40
  }
67
41
 
68
- const ctaSchema = z.object({ label: z.string(), href: z.string().optional() })
69
-
70
- // Mirrors the StorefrontHero block contract (FeaturedProduct) so a product-shape
71
- // mismatch is a type error at the catalog boundary, not a runtime crash. The
72
- // block requires every field — including `colors` — to be defined at render.
73
- const featuredProductSchema = z.object({
74
- id: z.string(),
75
- name: z.string(),
76
- brand: z.string(),
77
- price: z.number(),
78
- originalPrice: z.number(),
79
- rating: z.number(),
80
- reviewCount: z.number(),
81
- inStock: z.boolean(),
82
- isNew: z.boolean(),
83
- isOnSale: z.boolean(),
84
- image: z.string(),
85
- colors: z.array(z.string()),
86
- })
87
-
88
42
  const catalog = {
89
43
  StorefrontHero: {
90
- props: z.object({
91
- title: z.string(),
92
- description: z.string(),
93
- titleAccent: z.string().optional(),
94
- badge: z.string().optional(),
95
- products: z.array(featuredProductSchema),
96
- primaryCta: ctaSchema.optional(),
97
- secondaryCta: ctaSchema.optional(),
98
- }),
44
+ props: storefrontHeroPropsSchema,
99
45
  description: 'Landing hero.',
100
46
  },
101
47
  Feature: {
102
- props: z.object({
103
- items: z.array(z.record(z.string(), z.unknown())),
104
- badge: z.string().optional(),
105
- title: z.string().optional(),
106
- description: z.string().optional(),
107
- }),
48
+ props: featurePropsSchema,
108
49
  description: 'Benefit highlights.',
109
50
  },
110
51
  CategoryCards: {
111
- props: z.object({
112
- title: z.string().optional(),
113
- description: z.string().optional(),
114
- categories: z.array(z.record(z.string(), z.unknown())),
115
- cta: ctaSchema.optional(),
116
- }),
52
+ props: categoryCardsPropsSchema,
117
53
  description: 'Category grid.',
118
54
  },
119
55
  }
@@ -135,8 +71,44 @@ export const home: ViewModule = {
135
71
  },
136
72
  loader: {
137
73
  home: async () => {
138
- const { products, categories } = await getCatalog()
139
- return { heroProducts: products.slice(0, 4).map(toFeaturedProduct), categories }
74
+ // Data-bound sections resolve independently through the canonical catalog
75
+ // bridge; equal queries share request-local work and differing contexts
76
+ // stay isolated. A section failure never erases its siblings, and it is
77
+ // resolved separately so a catalog failure still yields truthful
78
+ // per-section state instead of a missing one.
79
+ let sections: Record<string, unknown> = {}
80
+ try {
81
+ sections = await resolveSectionViewStates(
82
+ [],
83
+ resolveSectionSources(homeData),
84
+ getStoreContext(),
85
+ )
86
+ } catch {
87
+ sections = {}
88
+ }
89
+ try {
90
+ const { products, categories, source } = await getCatalog()
91
+ return {
92
+ heroProducts: homeData.selectHeroProducts(products).map(toFeaturedProduct),
93
+ categories: categories.map(toCategoryCard),
94
+ navCategories: homeData.toNavCategories(categories),
95
+ dataSource: source,
96
+ dataError: null,
97
+ sections,
98
+ }
99
+ } catch (error) {
100
+ // A real-mode failure is surfaced as a visible error, never as fictional
101
+ // data. Mock mode is labelled by `dataSource`.
102
+ return {
103
+ heroProducts: [],
104
+ categories: [],
105
+ navCategories: [],
106
+ dataSource: getDataSource(),
107
+ dataError:
108
+ error instanceof Error ? error.message : 'No se pudo cargar el catálogo.',
109
+ sections,
110
+ }
111
+ }
140
112
  },
141
113
  },
142
114
  }
@@ -0,0 +1,22 @@
1
+ // Thor Commerce view props: home
2
+ // User-owned element props for the home view. Reconcile seeds this file once and
3
+ // never rewrites it; the derived view module (./index.tsx) imports these objects
4
+ // and binds them into the element tree, so edits here survive `storefront sync`
5
+ // and unit removal. Plain data only: no React and no server imports.
6
+
7
+ import { benefitsContent, categoriesContent, heroContent, heroImage } from './content'
8
+
9
+ export const heroProps = {
10
+ ...heroContent,
11
+ products: { $state: '/heroProducts' },
12
+ heroImage,
13
+ }
14
+
15
+ export const benefitsProps = {
16
+ ...benefitsContent,
17
+ }
18
+
19
+ export const categoriesProps = {
20
+ ...categoriesContent,
21
+ categories: { $state: '/categories' },
22
+ }
@@ -5,31 +5,43 @@
5
5
  // @json-render/react and components from @thorprovider/blocks — both must stay
6
6
  // on the client side of the RSC graph. Kept separate from index.tsx so the view
7
7
  // spec (elements/catalog/route/loader) stays a pure Server Component module.
8
+ //
9
+ // Props arrive as untyped spec data: every entry parses them through the view's
10
+ // schema (./schema.ts) before binding, so a contract mismatch fails loudly at
11
+ // the boundary instead of rendering undefined fields or literal icon tags.
8
12
 
9
13
  import {
10
14
  Feature1 as FeatureBlock,
11
15
  ProductCategory2 as CategoryCardsBlock,
12
16
  StorefrontHero1 as StorefrontHeroBlock,
13
17
  } from '@thorprovider/blocks'
14
- import type { FeaturedProduct } from '@thorprovider/blocks'
15
18
  import { useStateStore } from '@json-render/react'
19
+ import { resolveIcon } from '../../icon-map'
16
20
  import type { ViewRegistry } from '../types'
21
+ import {
22
+ categoryCardsPropsSchema,
23
+ featurePropsSchema,
24
+ storefrontHeroPropsSchema,
25
+ } from './schema'
17
26
 
18
27
  export const registry: ViewRegistry = {
19
28
  StorefrontHero: ({ props, emit }) => {
20
29
  const { set } = useStateStore()
30
+ const hero = storefrontHeroPropsSchema.parse(props)
21
31
  return (
22
32
  <StorefrontHeroBlock
23
- title={props.title as string}
24
- description={props.description as string}
25
- products={props.products as FeaturedProduct[]}
26
- titleAccent={props.titleAccent as string}
27
- badge={props.badge as string}
28
- primaryCta={props.primaryCta as never}
29
- secondaryCta={props.secondaryCta as never}
33
+ title={hero.title}
34
+ description={hero.description}
35
+ products={hero.products}
36
+ titleAccent={hero.titleAccent}
37
+ badge={hero.badge}
38
+ primaryCta={hero.primaryCta}
39
+ secondaryCta={hero.secondaryCta}
40
+ heroImage={hero.heroImage}
30
41
  onAddToCart={(product) => {
42
+ // Product-card shortcut: add the first purchasable variant.
31
43
  set('/lastClicked', product)
32
- emit('addToCart')
44
+ emit('addClickedToCart')
33
45
  }}
34
46
  onBuyNow={(product) => {
35
47
  set('/lastClicked', product)
@@ -39,24 +51,28 @@ export const registry: ViewRegistry = {
39
51
  />
40
52
  )
41
53
  },
42
- Feature: ({ props }) => (
43
- <FeatureBlock
44
- items={props.items as never}
45
- badge={props.badge as string}
46
- title={props.title as string}
47
- description={props.description as string}
48
- />
49
- ),
54
+ Feature: ({ props }) => {
55
+ const feature = featurePropsSchema.parse(props)
56
+ return (
57
+ <FeatureBlock
58
+ items={feature.items.map((item) => ({ ...item, icon: resolveIcon(item.icon) }))}
59
+ badge={feature.badge}
60
+ title={feature.title}
61
+ description={feature.description}
62
+ />
63
+ )
64
+ },
50
65
  CategoryCards: ({ props, emit }) => {
51
66
  const { set } = useStateStore()
67
+ const cards = categoryCardsPropsSchema.parse(props)
52
68
  return (
53
69
  <CategoryCardsBlock
54
- title={props.title as string}
55
- description={props.description as string}
56
- categories={props.categories as never}
57
- cta={props.cta as never}
70
+ title={cards.title}
71
+ description={cards.description}
72
+ categories={cards.categories}
73
+ cta={cards.cta}
58
74
  onBrowse={(category) => {
59
- set('/lastCategory', (category.handle as string) ?? String(category.id))
75
+ set('/lastCategory', category.handle ?? String(category.id))
60
76
  emit('openCategory')
61
77
  }}
62
78
  />