@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,93 @@
1
+ // Thor Commerce view schema fragment: home
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.
7
+
8
+ import { z } from 'zod'
9
+
10
+ export const ctaSchema = z.object({
11
+ label: z.string(),
12
+ href: z.string().optional(),
13
+ })
14
+
15
+ // Mirrors the StorefrontHero block contract (FeaturedProduct): the block
16
+ // requires every field — including `colors` — to be defined at render.
17
+ export const featuredProductSchema = z.object({
18
+ id: z.string(),
19
+ name: z.string(),
20
+ brand: z.string(),
21
+ price: z.number(),
22
+ originalPrice: z.number(),
23
+ rating: z.number(),
24
+ reviewCount: z.number(),
25
+ inStock: z.boolean(),
26
+ isNew: z.boolean(),
27
+ isOnSale: z.boolean(),
28
+ image: z.string(),
29
+ colors: z.array(z.string()),
30
+ })
31
+
32
+ // Mirrors the Feature1 block contract; `icon` is a NAME here and only becomes
33
+ // a component when the registry resolves it through the shared icon map.
34
+ export const featureItemSchema = z.object({
35
+ id: z.string(),
36
+ title: z.string(),
37
+ description: z.string(),
38
+ icon: z.string(),
39
+ })
40
+
41
+ // Mirrors the ProductCategory2 block contract (ProductCategory2Category).
42
+ // `itemCount` is optional: when the backend count is unknown the projection
43
+ // omits it (the block renders no count) rather than fabricating a `0`. A real
44
+ // `0` is preserved.
45
+ export const categoryCardSchema = z.object({
46
+ id: z.string(),
47
+ handle: z.string().optional(),
48
+ name: z.string(),
49
+ description: z.string(),
50
+ itemCount: z.number().optional(),
51
+ image: z.string(),
52
+ trending: z.boolean(),
53
+ })
54
+
55
+ // Optional editorial hero image (mirrors the StorefrontHero block contract).
56
+ // Rendered as a native `<img>` only when `src` is a non-empty string; omitted
57
+ // entirely when absent, so a blank `src` is never emitted.
58
+ export const heroImageSchema = z.object({
59
+ src: z.string(),
60
+ alt: z.string(),
61
+ })
62
+
63
+ export const storefrontHeroPropsSchema = z.object({
64
+ title: z.string(),
65
+ description: z.string(),
66
+ titleAccent: z.string().optional(),
67
+ badge: z.string().optional(),
68
+ products: z.array(featuredProductSchema),
69
+ primaryCta: ctaSchema.optional(),
70
+ secondaryCta: ctaSchema.optional(),
71
+ heroImage: heroImageSchema.optional(),
72
+ })
73
+
74
+ export const featurePropsSchema = z.object({
75
+ items: z.array(featureItemSchema),
76
+ badge: z.string().optional(),
77
+ title: z.string().optional(),
78
+ description: z.string().optional(),
79
+ })
80
+
81
+ export const categoryCardsPropsSchema = z.object({
82
+ title: z.string().optional(),
83
+ description: z.string().optional(),
84
+ categories: z.array(categoryCardSchema),
85
+ cta: ctaSchema.optional(),
86
+ })
87
+
88
+ export type FeaturedProduct = z.infer<typeof featuredProductSchema>
89
+ export type FeatureItem = z.infer<typeof featureItemSchema>
90
+ export type CategoryCard = z.infer<typeof categoryCardSchema>
91
+ export type StorefrontHeroProps = z.infer<typeof storefrontHeroPropsSchema>
92
+ export type FeatureProps = z.infer<typeof featurePropsSchema>
93
+ export type CategoryCardsProps = z.infer<typeof categoryCardsPropsSchema>
@@ -0,0 +1,25 @@
1
+ // Thor Commerce view content: product
2
+ // User-owned view content — the labels rendered by the product overview. No
3
+ // reviews are seeded: reviews are rendered only when the backend supplies real
4
+ // ones, so this file must never carry fabricated review data. Reconcile seeds
5
+ // this file once and never rewrites it; the derived view module (./index.tsx)
6
+ // imports it and wires it into the element tree, so your edits here survive
7
+ // `storefront sync` and unit removal. Plain data only: no React and no server
8
+ // imports.
9
+
10
+ export const productLabels = {
11
+ sizesLabel: 'Variantes',
12
+ variantLabel: 'Variante',
13
+ reviewsLabel: 'Reseñas',
14
+ addToCartLabel: 'Agregar al carrito',
15
+ outOfStockLabel: 'Agotado',
16
+ unavailablePriceLabel: 'Precio no disponible',
17
+ selectVariantLabel: 'Selecciona una variante',
18
+ quantityLabel: 'Cantidad',
19
+ }
20
+
21
+ // Copy for the reviews region. It renders only when the backend supplies real
22
+ // reviews (`/hasProductReviews`); the list itself is never seeded here.
23
+ export const reviewsCopy = {
24
+ pageTitle: 'Opiniones de clientes',
25
+ }
@@ -0,0 +1,50 @@
1
+ // Thor Commerce view data: product
2
+ // User-owned loader shaping for the product view. Reconcile seeds this file once
3
+ // and never rewrites it; the derived view module (./index.tsx) imports these
4
+ // helpers and calls them from its loader, so edits here survive `storefront sync`
5
+ // and unit removal. Pure functions only: no React and no server imports.
6
+
7
+ import type { CatalogProductDetail } from '@thorprovider/runtime/commerce'
8
+ import type { ProductDetailProduct } from '@thorprovider/blocks'
9
+
10
+ /**
11
+ * Map the runtime's canonical product detail (real variants, options,
12
+ * availability, Money) to the ProductOverview1 block contract.
13
+ *
14
+ * Price availability is preserved, not collapsed: a variant whose backend price
15
+ * is unresolved carries `price: null` (the block treats that as unavailable and
16
+ * disables purchase), while a backend-resolved valid zero stays `"0"`.
17
+ */
18
+ export function buildSelectedProduct(
19
+ detail: CatalogProductDetail | null | undefined,
20
+ ): ProductDetailProduct | null {
21
+ if (!detail) return null
22
+ return {
23
+ id: detail.id,
24
+ handle: detail.handle,
25
+ title: detail.title,
26
+ description: detail.description,
27
+ options: detail.options,
28
+ variants: detail.variants.map((variant) => ({
29
+ id: variant.id,
30
+ title: variant.title,
31
+ sku: variant.sku,
32
+ availableForSale: variant.availableForSale,
33
+ selectedOptions: variant.options,
34
+ price: variant.price.status === 'available' ? variant.price.money : null,
35
+ image: variant.image,
36
+ })),
37
+ images: detail.images,
38
+ featuredImage: detail.featuredImage,
39
+ }
40
+ }
41
+
42
+ /**
43
+ * No first-variant shortcut on the product detail surface: nothing is selected
44
+ * until the shopper explicitly chooses a variant, so the detail's `onAddToCart`
45
+ * can never silently add the first one. Listing cards use the separate
46
+ * first-purchasable-variant shortcut instead.
47
+ */
48
+ export function initialSelectedVariantId(): string | null {
49
+ return null
50
+ }
@@ -0,0 +1,148 @@
1
+ // Thor Commerce view module: product
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 -> "product".
6
+
7
+ import { notFound } from 'next/navigation'
8
+ import { loadProductDetail } from '../../commerce'
9
+ import { getDataSource } from '../../store-context'
10
+ import { mountedFeatureNames } from '../../composition'
11
+ import { pageRoot } from '../../spec/page-root'
12
+ import type { StorefrontElement } from '../../spec/types'
13
+ import type { ViewModule } from '../types'
14
+ import { registry } from './registry'
15
+ import { productBreadcrumbProps, productOverviewProps, productReviewsProps } from './props'
16
+ import { buildSelectedProduct, initialSelectedVariantId } from './data'
17
+ import { breadcrumbPropsSchema, productOverviewPropsSchema, reviewsPropsSchema } from './schema'
18
+
19
+ // The element tree is derived wiring; the rendered props come from the
20
+ // user-owned ./props.ts module so they survive sync and unit removal.
21
+ const elements: Record<string, StorefrontElement> = {
22
+ // The navigability feature contributes the entity-derived breadcrumb trail.
23
+ // The element is always wired; the loader returns an empty trail when the
24
+ // feature is not installed or the backend supplies no category, so the block
25
+ // renders nothing and removal leaves no dangling navigation aid.
26
+ 'product.breadcrumb': {
27
+ type: 'Breadcrumb',
28
+ props: {
29
+ ...productBreadcrumbProps,
30
+ items: { $state: '/breadcrumbItems' },
31
+ },
32
+ },
33
+ 'product.overview': {
34
+ type: 'ProductOverview',
35
+ visible: { $state: '/selectedProduct' },
36
+ props: productOverviewProps,
37
+ on: { addToCart: { action: 'addToCart' } },
38
+ },
39
+ // Rendered only when the backend supplies real reviews; the list is never
40
+ // seeded, so an unsupplied review is not shown and no placeholder is invented.
41
+ 'product.reviews': {
42
+ type: 'Reviews',
43
+ visible: { $state: '/hasProductReviews' },
44
+ props: productReviewsProps,
45
+ on: {
46
+ submitReview: { action: 'submitReview' },
47
+ toggleReviewLike: { action: 'toggleReviewLike' },
48
+ },
49
+ },
50
+ }
51
+
52
+ const catalog = {
53
+ Breadcrumb: {
54
+ props: breadcrumbPropsSchema,
55
+ description: 'Migas de pan derivadas de la jerarquía real de categorías del producto.',
56
+ },
57
+ ProductOverview: {
58
+ props: productOverviewPropsSchema,
59
+ description: 'Detalle de producto: galería, precio, variantes y añadir al carrito.',
60
+ },
61
+ Reviews: {
62
+ props: reviewsPropsSchema,
63
+ description: 'Opiniones de clientes con resumen de valoración.',
64
+ },
65
+ }
66
+
67
+ const hasNavigability = mountedFeatureNames.includes('navigability')
68
+
69
+ // Entity-derived trail only: the product's real ancestor categories. When the
70
+ // feature is absent or the backend supplies no category, the trail is empty and
71
+ // the Breadcrumb block renders nothing (no level is ever invented).
72
+ function resolveProductBreadcrumbItems(
73
+ title: string,
74
+ categoryPath: { handle: string; name: string }[] | undefined,
75
+ ): { label: string; href?: string }[] {
76
+ if (!hasNavigability || !categoryPath || categoryPath.length === 0) return []
77
+ const chain = categoryPath.map((entry) => ({
78
+ label: entry.name,
79
+ href: `/categories/${entry.handle}`,
80
+ }))
81
+ // Home > ancestor categories (links) > the product itself (current page).
82
+ return [{ label: 'Inicio', href: '/' }, ...chain, { label: title }]
83
+ }
84
+
85
+ export const product: ViewModule = {
86
+ name: 'product',
87
+ elements,
88
+ catalog,
89
+ registry,
90
+ route: {
91
+ path: '/products/[id]',
92
+ loaderName: 'product',
93
+ layout: 'default',
94
+ metadata: {
95
+ title: 'Producto',
96
+ description: 'Detalles del producto seleccionado.',
97
+ },
98
+ entry: pageRoot(
99
+ 'product',
100
+ ['product.breadcrumb', 'product.overview', 'product.reviews'],
101
+ elements,
102
+ ),
103
+ },
104
+ loader: {
105
+ product: async (params) => {
106
+ const handle = Array.isArray(params.id) ? params.id[0] : params.id
107
+ const dataSource = getDataSource()
108
+
109
+ let result: Awaited<ReturnType<typeof loadProductDetail>>
110
+ try {
111
+ result = await loadProductDetail(String(handle))
112
+ } catch (error) {
113
+ // A real data-source failure is surfaced visibly (never demo data).
114
+ return {
115
+ selectedProduct: null,
116
+ selectedVariantId: null,
117
+ productQuantity: 1,
118
+ productReviews: [],
119
+ hasProductReviews: false,
120
+ breadcrumbItems: [],
121
+ dataSource,
122
+ dataError:
123
+ error instanceof Error ? error.message : 'No se pudo cargar el producto.',
124
+ }
125
+ }
126
+
127
+ // An unknown handle is a real 404 (the catch-all page renders not-found);
128
+ // it is never a demo product.
129
+ if (!result.detail) notFound()
130
+
131
+ return {
132
+ selectedProduct: buildSelectedProduct(result.detail),
133
+ // No first-variant shortcut on the detail surface: the shopper must
134
+ // select explicitly (listing cards use the card shortcut instead).
135
+ selectedVariantId: initialSelectedVariantId(),
136
+ productQuantity: 1,
137
+ productReviews: [],
138
+ hasProductReviews: false,
139
+ breadcrumbItems: resolveProductBreadcrumbItems(
140
+ result.detail.title,
141
+ result.detail.categoryPath,
142
+ ),
143
+ dataSource: result.source,
144
+ dataError: null,
145
+ }
146
+ },
147
+ },
148
+ }
@@ -0,0 +1,27 @@
1
+ // Thor Commerce view props: product
2
+ // User-owned element props for the product 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 { productLabels, reviewsCopy } from './content'
9
+
10
+ export const productOverviewProps = {
11
+ ...productLabels,
12
+ product: { $state: '/selectedProduct' },
13
+ selectedVariantId: { $state: '/selectedVariantId' },
14
+ quantity: { $state: '/productQuantity' },
15
+ }
16
+
17
+ export const productReviewsProps = {
18
+ ...reviewsCopy,
19
+ reviews: { $state: '/productReviews' },
20
+ }
21
+
22
+ // Presentational defaults only; the entity-derived trail is bound to
23
+ // `$state: '/productBreadcrumbItems'` in the element tree, never invented here.
24
+ export const productBreadcrumbProps = {
25
+ ariaLabel: 'Migas de pan',
26
+ backLabel: 'Volver',
27
+ }
@@ -0,0 +1,103 @@
1
+ 'use client'
2
+
3
+ // Thor Commerce view registry fragment: product
4
+ // Client boundary for the product 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 {
10
+ Breadcrumb as BreadcrumbBlock,
11
+ ProductOverview1 as ProductOverviewBlock,
12
+ ReviewRating1 as ReviewsBlock,
13
+ } from '@thorprovider/blocks'
14
+ import { isAvailableMoney } from '@thorprovider/blocks/contracts'
15
+ import { useStateStore } from '@json-render/react'
16
+ import { useProductVisitObservation } from '../../section-observation'
17
+ import type { ViewRegistry } from '../types'
18
+ import { productOverviewPropsSchema, reviewsPropsSchema } from './schema'
19
+
20
+ export const registry: ViewRegistry = {
21
+ Breadcrumb: ({ props }) => {
22
+ const p = props as {
23
+ items: { label: string; href?: string }[]
24
+ maxItems?: number
25
+ ariaLabel?: string
26
+ backLabel?: string
27
+ className?: string
28
+ }
29
+ return (
30
+ <BreadcrumbBlock
31
+ items={p.items}
32
+ maxItems={p.maxItems}
33
+ ariaLabel={p.ariaLabel}
34
+ backLabel={p.backLabel}
35
+ className={p.className}
36
+ />
37
+ )
38
+ },
39
+ ProductOverview: ({ props, emit }) => {
40
+ const { set } = useStateStore()
41
+ const parsed = productOverviewPropsSchema.parse(props)
42
+ // A successfully rendered product destination: recorded only when it matches
43
+ // the section origin (disabled by default; never attributes a guess). The
44
+ // stable public identity is the handle, which is what section clicks record.
45
+ const productHandle = parsed.product.handle ?? parsed.product.id
46
+ useProductVisitObservation(productHandle, `/products/${productHandle}`)
47
+ return (
48
+ <ProductOverviewBlock
49
+ product={parsed.product}
50
+ selectedVariantId={parsed.selectedVariantId ?? null}
51
+ quantity={parsed.quantity}
52
+ sizesLabel={parsed.sizesLabel}
53
+ variantLabel={parsed.variantLabel}
54
+ reviewsLabel={parsed.reviewsLabel}
55
+ addToCartLabel={parsed.addToCartLabel}
56
+ outOfStockLabel={parsed.outOfStockLabel}
57
+ unavailablePriceLabel={parsed.unavailablePriceLabel}
58
+ selectVariantLabel={parsed.selectVariantLabel}
59
+ quantityLabel={parsed.quantityLabel}
60
+ onSelectVariant={(variantId) => {
61
+ set('/selectedVariantId', variantId)
62
+ }}
63
+ onQuantityChange={(quantity) => {
64
+ set('/productQuantity', quantity)
65
+ }}
66
+ onAddToCart={(variantId, quantity) => {
67
+ // Preserve the selected backend variant id and quantity the block
68
+ // emits — the handler must never drop them or substitute the SKU.
69
+ const variant = parsed.product.variants.find((candidate) => candidate.id === variantId)
70
+ const price = isAvailableMoney(variant?.price) ? Number(variant.price.amount) : 0
71
+ set('/lastAddToCart', {
72
+ variantId,
73
+ quantity,
74
+ name: parsed.product.title,
75
+ variant: variant?.title ?? '',
76
+ price,
77
+ image: variant?.image?.url ?? parsed.product.featuredImage?.url,
78
+ })
79
+ emit('addToCart')
80
+ }}
81
+ onToggleWishlist={() => {}}
82
+ />
83
+ )
84
+ },
85
+ Reviews: ({ props, emit }) => {
86
+ const parsed = reviewsPropsSchema.parse(props)
87
+ return (
88
+ <ReviewsBlock
89
+ pageTitle={parsed.pageTitle}
90
+ pageDescription={parsed.pageDescription}
91
+ reviews={parsed.reviews}
92
+ form={parsed.form}
93
+ reviewsPerPage={parsed.reviewsPerPage}
94
+ onToggleReviewLike={() => {
95
+ emit('toggleReviewLike')
96
+ }}
97
+ onSubmitReview={() => {
98
+ emit('submitReview')
99
+ }}
100
+ />
101
+ )
102
+ },
103
+ }
@@ -0,0 +1,121 @@
1
+ // Thor Commerce view schema fragment: product
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
+ const imageSchema = z.object({
13
+ id: z.string().optional(),
14
+ url: z.string(),
15
+ altText: z.string(),
16
+ width: z.number().optional(),
17
+ height: z.number().optional(),
18
+ })
19
+
20
+ // Canonical Money: a decimal string in major units ("69.99"), never cents.
21
+ const moneySchema = z.object({
22
+ amount: z.string(),
23
+ currencyCode: z.string(),
24
+ })
25
+
26
+ const selectedOptionSchema = z.object({
27
+ name: z.string(),
28
+ value: z.string(),
29
+ })
30
+
31
+ // Mirrors ProductDetailVariant: `price` is absent/null when the backend has no
32
+ // resolved price (the block disables purchase); a valid zero stays "0".
33
+ const productDetailVariantSchema = z.object({
34
+ id: z.string(),
35
+ title: z.string(),
36
+ sku: z.string().optional(),
37
+ availableForSale: z.boolean(),
38
+ selectedOptions: z.array(selectedOptionSchema),
39
+ price: moneySchema.nullish(),
40
+ compareAtPrice: moneySchema.optional(),
41
+ image: imageSchema.optional(),
42
+ })
43
+
44
+ // Mirrors ProductDetailProduct (the ProductOverview1 block contract).
45
+ export const productDetailProductSchema = z.object({
46
+ id: z.string(),
47
+ handle: z.string().optional(),
48
+ title: z.string(),
49
+ description: z.string().optional(),
50
+ vendor: z.string().optional(),
51
+ options: z.array(z.object({ id: z.string(), name: z.string(), values: z.array(z.string()) })),
52
+ variants: z.array(productDetailVariantSchema),
53
+ images: z.array(imageSchema),
54
+ featuredImage: imageSchema.optional(),
55
+ })
56
+
57
+ export const productOverviewPropsSchema = z.object({
58
+ product: productDetailProductSchema,
59
+ selectedVariantId: z.string().nullable().optional(),
60
+ quantity: z.number().optional(),
61
+ sizesLabel: z.string().optional(),
62
+ variantLabel: z.string().optional(),
63
+ reviewsLabel: z.string().optional(),
64
+ addToCartLabel: z.string().optional(),
65
+ outOfStockLabel: z.string().optional(),
66
+ unavailablePriceLabel: z.string().optional(),
67
+ selectVariantLabel: z.string().optional(),
68
+ quantityLabel: z.string().optional(),
69
+ })
70
+
71
+ // Mirrors ReviewRating1Review (real reviews only; never seeded).
72
+ export const reviewSchema = z.object({
73
+ id: z.number(),
74
+ userName: z.string(),
75
+ userAvatar: z.string().optional(),
76
+ date: z.string(),
77
+ rating: z.number(),
78
+ content: z.string(),
79
+ likes: z.number().optional(),
80
+ comments: z.number().optional(),
81
+ })
82
+
83
+ export const reviewFormSchema = z.object({
84
+ title: z.string(),
85
+ description: z.string(),
86
+ nameLabel: z.string(),
87
+ emailLabel: z.string(),
88
+ reviewLabel: z.string(),
89
+ namePlaceholder: z.string(),
90
+ emailPlaceholder: z.string(),
91
+ reviewPlaceholder: z.string(),
92
+ submitButtonText: z.string(),
93
+ submittingButtonText: z.string(),
94
+ })
95
+
96
+ export const reviewsPropsSchema = z.object({
97
+ pageTitle: z.string(),
98
+ pageDescription: z.string().optional(),
99
+ reviews: z.array(reviewSchema),
100
+ form: reviewFormSchema.optional(),
101
+ reviewsPerPage: z.number().optional(),
102
+ })
103
+
104
+ export const breadcrumbItemSchema = z.object({
105
+ label: z.string(),
106
+ href: z.string().optional(),
107
+ })
108
+
109
+ export const breadcrumbPropsSchema = z.object({
110
+ items: z.array(breadcrumbItemSchema),
111
+ maxItems: z.number().int().min(2).optional(),
112
+ ariaLabel: z.string().optional(),
113
+ backLabel: z.string().optional(),
114
+ className: z.string().optional(),
115
+ })
116
+
117
+ export type ProductDetailProductSpec = z.infer<typeof productDetailProductSchema>
118
+ export type ProductOverviewProps = z.infer<typeof productOverviewPropsSchema>
119
+ export type Review = z.infer<typeof reviewSchema>
120
+ export type ReviewForm = z.infer<typeof reviewFormSchema>
121
+ export type ReviewsProps = z.infer<typeof reviewsPropsSchema>
@@ -0,0 +1,99 @@
1
+ // Thor Commerce view content: static
2
+ // User-owned content — reconcile seeds this file once and never rewrites it, so
3
+ // your edits survive `storefront sync`. Declare the pages this store serves and
4
+ // the ordered sections each one renders. The derived view builds one fixed route
5
+ // per declared page, so an undeclared path falls through to the not-found
6
+ // response.
7
+ //
8
+ // Neutral, truthful copy only: these pages make no delivery, shipping, or price
9
+ // promise and invent no company facts. Replace the copy with your own before
10
+ // publishing.
11
+
12
+ import type { StaticPageContent } from './schema'
13
+
14
+ export const staticPagesContent: StaticPageContent[] = [
15
+ {
16
+ path: '/contact',
17
+ title: 'Contacto',
18
+ description: 'Cómo comunicarte con la tienda.',
19
+ sections: [
20
+ { kind: 'heading', text: 'Contacto' },
21
+ {
22
+ kind: 'paragraph',
23
+ text: 'Esta tienda atiende en línea. Usa los canales de atención que la tienda publique para resolver tus dudas sobre productos, pedidos y pagos.',
24
+ },
25
+ {
26
+ kind: 'list',
27
+ items: [
28
+ 'Consulta el estado de tu pedido desde tu cuenta.',
29
+ 'Escríbenos por el canal de atención que la tienda publique.',
30
+ 'Revisa las preguntas frecuentes antes de escribirnos.',
31
+ ],
32
+ },
33
+ ],
34
+ },
35
+ {
36
+ path: '/terms',
37
+ title: 'Términos y condiciones',
38
+ description: 'Condiciones de uso de la tienda en línea.',
39
+ sections: [
40
+ { kind: 'heading', text: 'Términos y condiciones' },
41
+ {
42
+ kind: 'paragraph',
43
+ text: 'Al usar esta tienda en línea aceptas estas condiciones de uso. Léelas con atención antes de realizar una compra.',
44
+ },
45
+ {
46
+ kind: 'paragraph',
47
+ text: 'La tienda puede actualizar estos términos para reflejar cambios en el servicio. La versión vigente es la publicada en esta página.',
48
+ },
49
+ {
50
+ kind: 'list',
51
+ items: [
52
+ 'Usa la tienda de forma lícita y con información veraz.',
53
+ 'El catálogo, los precios y la disponibilidad se muestran en la tienda y pueden cambiar.',
54
+ 'El proceso de compra se completa siguiendo los pasos indicados en el sitio.',
55
+ ],
56
+ },
57
+ ],
58
+ },
59
+ {
60
+ path: '/privacy',
61
+ title: 'Política de privacidad',
62
+ description: 'Cómo se tratan los datos en la tienda en línea.',
63
+ sections: [
64
+ { kind: 'heading', text: 'Política de privacidad' },
65
+ {
66
+ kind: 'paragraph',
67
+ text: 'Esta política describe cómo la tienda trata la información que proporcionas al navegar y al comprar.',
68
+ },
69
+ {
70
+ kind: 'paragraph',
71
+ text: 'Usamos tus datos para gestionar tu cuenta, procesar tus pedidos y darte soporte. No los usamos para fines distintos sin tu consentimiento.',
72
+ },
73
+ {
74
+ kind: 'list',
75
+ items: [
76
+ 'Proporciona solo la información necesaria para completar tu compra.',
77
+ 'Puedes solicitar la actualización o eliminación de tus datos por los canales de atención.',
78
+ 'Conserva tus credenciales de acceso de forma segura.',
79
+ ],
80
+ },
81
+ ],
82
+ },
83
+ {
84
+ path: '/about',
85
+ title: 'Sobre nosotros',
86
+ description: 'Información sobre la tienda.',
87
+ sections: [
88
+ { kind: 'heading', text: 'Sobre nosotros' },
89
+ {
90
+ kind: 'paragraph',
91
+ text: 'Somos una tienda en línea que ofrece un catálogo de productos para tu hogar y tu negocio.',
92
+ },
93
+ {
94
+ kind: 'paragraph',
95
+ text: 'Trabajamos para que encuentres lo que buscas y para acompañarte durante todo el proceso de compra a través de esta tienda.',
96
+ },
97
+ ],
98
+ },
99
+ ]