@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,257 @@
1
+ // Generated by Thor Commerce storefront (composition core: state)
2
+ // Shared, feature-independent storefront state. Installed features add their
3
+ // own slices; `state.ts` (the composer) spreads them over `storefrontCoreState`.
4
+ //
5
+ // The cart slice mirrors the backend-authoritative projection: line totals and
6
+ // cart totals are canonical `Money` from the backend, never locally computed.
7
+
8
+ import type {
9
+ CardProduct,
10
+ FeaturedProduct,
11
+ ProductCategory2Category,
12
+ ProductDetailProduct,
13
+ } from '@thorprovider/blocks'
14
+ import type { Money, PaymentMethod } from '@thorprovider/types'
15
+
16
+ /** Real review, rendered only when the backend supplies it. */
17
+ export interface ProductReview {
18
+ id: number
19
+ userName: string
20
+ userAvatar?: string
21
+ date: string
22
+ rating: number
23
+ content: string
24
+ likes?: number
25
+ comments?: number
26
+ }
27
+
28
+ /** A cart line projected from the backend cart. */
29
+ export interface CartLineState {
30
+ /** Backend cart line-item id — the update/removal identity. */
31
+ id: string
32
+ /** Backend variant id — the add identity (carried for line updates). */
33
+ merchandiseId: string
34
+ name: string
35
+ variant?: string
36
+ quantity: number
37
+ /** Backend line total; rendered verbatim, never `unit × qty` locally. */
38
+ lineTotal: Money
39
+ /** Backend unit price, when supplied; never derived from `lineTotal / qty`. */
40
+ unitPrice?: Money
41
+ /** Normalized optional image; omitted when the backend supplies none. */
42
+ image?: string
43
+ handle?: string
44
+ }
45
+
46
+ /** Backend-authoritative cart totals. */
47
+ export interface CartTotalsState {
48
+ subtotal: Money
49
+ discount?: Money
50
+ tax?: Money
51
+ shipping?: Money
52
+ total: Money
53
+ }
54
+
55
+ /** Mirrors the CheckoutForm1FormData block contract (card fields stay unused). */
56
+ export interface CheckoutFormData {
57
+ email: string
58
+ firstName: string
59
+ lastName: string
60
+ phone: string
61
+ address: string
62
+ city: string
63
+ state: string
64
+ zipCode: string
65
+ country: string
66
+ paymentMethod: string
67
+ cardNumber: string
68
+ expiryDate: string
69
+ cvv: string
70
+ cardName: string
71
+ saveInfo: boolean
72
+ sameAsBilling: boolean
73
+ newsletter: boolean
74
+ promoCode: string
75
+ }
76
+
77
+ /** An eligible backend shipping option. */
78
+ export interface CheckoutShippingOption {
79
+ id: string
80
+ name: string
81
+ price?: Money
82
+ description?: string
83
+ estimatedDays?: string
84
+ }
85
+
86
+ /** Structured per-field checkout errors, mirroring the block contract. */
87
+ export type CheckoutFieldErrors = Record<string, { message?: string }[]>
88
+
89
+ /** Confirmation order, mirroring the CheckoutConfirmation block contract. */
90
+ export interface StorefrontOrder {
91
+ number: string
92
+ placedAt: string
93
+ email?: string
94
+ currentStage?: 'Confirmed' | 'Packed' | 'Shipped' | 'Delivered'
95
+ estimatedDelivery?: string | null
96
+ trackingHref?: string
97
+ shipping?: { name: string; lines: string[]; method?: string }
98
+ billing?: { name: string; lines: string[]; method?: string }
99
+ totals: { subtotal?: Money; discount?: Money; shipping?: Money; tax?: Money; total?: Money }
100
+ }
101
+
102
+ /** Confirmation line: backend Money line total, optional image omitted when absent. */
103
+ export interface StorefrontOrderLineView {
104
+ id: string
105
+ name: string
106
+ variant?: string
107
+ quantity: number
108
+ lineTotal: Money
109
+ unitPrice?: Money
110
+ image?: string
111
+ }
112
+
113
+ /** A single line of a customer order-history row (display strings, no Money). */
114
+ export interface AccountOrderItemView {
115
+ id: string
116
+ name: string
117
+ variant?: string
118
+ quantity: number
119
+ lineTotal: string
120
+ image?: string
121
+ }
122
+
123
+ /**
124
+ * A customer order-history row, mapped from the backend order for the account
125
+ * view. Money is formatted to a display string from the order's own currency;
126
+ * an optional field the backend omits is omitted, never invented.
127
+ */
128
+ export interface AccountOrderView {
129
+ id: string
130
+ number: string
131
+ date: string
132
+ status: string
133
+ total: string
134
+ subtotal?: string
135
+ tax?: string
136
+ shipping?: string
137
+ address?: string
138
+ items: AccountOrderItemView[]
139
+ }
140
+
141
+ /**
142
+ * The completed-order receipt as ONE atomic value.
143
+ *
144
+ * Order and its backend lines live together so a single state write carries
145
+ * them, and the SSR initial value is a stable `null`. json-render's
146
+ * StateProvider re-applies the SSR `initialState` on every RSC refresh and
147
+ * updates any key whose flattened initial value changed by reference; a
148
+ * separate `lastOrderLines: []` array would be reset to a fresh empty array
149
+ * after a Server Action, dropping the backend lines. A single nullable object
150
+ * keeps the receipt out of that re-apply.
151
+ */
152
+ export interface StorefrontReceipt {
153
+ order: StorefrontOrder
154
+ lines: StorefrontOrderLineView[]
155
+ }
156
+
157
+ export interface NavCategory {
158
+ label: string
159
+ href: string
160
+ children?: { label: string; href: string }[]
161
+ emphasis?: boolean
162
+ }
163
+
164
+ export type OrderStatus = 'idle' | 'pending' | 'error' | 'unresolved' | 'complete'
165
+
166
+ export interface StorefrontCoreState {
167
+ heroProducts: FeaturedProduct[]
168
+ categories: ProductCategory2Category[]
169
+ navCategories: NavCategory[]
170
+ categoryProducts: CardProduct[]
171
+ /** Canonical product detail: real variants, options, availability, and Money. */
172
+ selectedProduct: ProductDetailProduct | null
173
+ /** Explicitly selected backend variant id; null until the shopper selects one. */
174
+ selectedVariantId: string | null
175
+ /** Requested add-to-cart quantity for the selected variant. */
176
+ productQuantity: number
177
+ /** Real reviews, supplied by the backend only. */
178
+ productReviews: ProductReview[]
179
+ hasProductReviews: boolean
180
+ lastClicked: CardProduct | null
181
+ lastCategory: string | null
182
+ bannerDismissed: boolean
183
+ /** True when the explicit data-source / error notice has been dismissed. */
184
+ dataNoticeDismissed: boolean
185
+ /** Explicit data source; `'mock'` is always labelled as demo data. */
186
+ dataSource: 'mock' | 'medusa'
187
+ /** Visible real-mode failure; null when the data loaded. */
188
+ dataError: string | null
189
+ cartOpen: boolean
190
+ // ---- cart (backend-authoritative) ----
191
+ cart: CartLineState[]
192
+ cartTotals: CartTotalsState | null
193
+ cartId: string | null
194
+ cartQuantity: number
195
+ cartCompleted: boolean
196
+ /** True when the verified session carries a completed receipt to recover. */
197
+ receiptAvailable: boolean
198
+ cartPending: boolean
199
+ cartError: string | null
200
+ /** Recovery/notice message (for example an unresolved completion). */
201
+ cartNotice: string | null
202
+ /** Shipping OPTION id (`so_`) the checkout UI selects/highlights. */
203
+ cartSelectedShippingOptionId: string | null
204
+ /** Visible backend cart-read failure; null when the cart read succeeded. */
205
+ cartReadError: string | null
206
+ }
207
+
208
+ /** Full storefront state: core plus the checkout feature's slice. */
209
+ export interface StorefrontState extends StorefrontCoreState {
210
+ checkout: CheckoutFormData
211
+ checkoutCountries: { value: string; label: string }[]
212
+ checkoutPaymentMethods: PaymentMethod[]
213
+ checkoutShippingOptions: CheckoutShippingOption[]
214
+ checkoutSelectedShippingOptionId: string | null
215
+ checkoutFieldErrors: CheckoutFieldErrors
216
+ checkoutPending: boolean
217
+ checkoutError: string | null
218
+ /**
219
+ * The completion discriminator behind `checkoutError`, preserved separately
220
+ * from `orderStatus`: a native cart-with-error (`cart-error`), a definite
221
+ * pre-completion failure (`error`), or an ambiguous completion (`unresolved`).
222
+ */
223
+ checkoutErrorKind: 'error' | 'cart-error' | 'unresolved' | null
224
+ /** Atomic completed-order receipt (order + backend lines), stable-null initial. */
225
+ lastReceipt: StorefrontReceipt | null
226
+ orderStatus: OrderStatus
227
+ }
228
+
229
+ export const storefrontCoreState: StorefrontCoreState = {
230
+ heroProducts: [],
231
+ categories: [],
232
+ navCategories: [],
233
+ categoryProducts: [],
234
+ selectedProduct: null,
235
+ selectedVariantId: null,
236
+ productQuantity: 1,
237
+ productReviews: [],
238
+ hasProductReviews: false,
239
+ lastClicked: null,
240
+ lastCategory: null,
241
+ bannerDismissed: false,
242
+ dataNoticeDismissed: false,
243
+ dataSource: 'mock',
244
+ dataError: null,
245
+ cartOpen: false,
246
+ cart: [],
247
+ cartTotals: null,
248
+ cartId: null,
249
+ cartQuantity: 0,
250
+ cartCompleted: false,
251
+ receiptAvailable: false,
252
+ cartPending: false,
253
+ cartError: null,
254
+ cartNotice: null,
255
+ cartSelectedShippingOptionId: null,
256
+ cartReadError: null,
257
+ }
@@ -1,35 +1,11 @@
1
- // Generated by Thor Commerce storefront (template: assets/templates/app/lib/__STOREFRONT__/state.ts)
2
- // Initial storefront state. Extended by recipes as views are added.
1
+ // Generated by Thor Commerce storefront (composition composer: state).
2
+ // Do not edit by hand — the manifest is the source of truth; run `storefront sync`.
3
+ // Installed features contribute their state slices below.
3
4
 
4
- import type { CardProduct, ProductCategory2Category } from '@thorprovider/blocks'
5
+ import { storefrontCoreState } from './state.core'
5
6
 
6
- export interface StoreOverviewProduct {
7
- id: string
8
- brand: string
9
- name: string
10
- description: string
11
- price: number
12
- currency: string
13
- rating: number
14
- images: { id: number; src: string; alt: string }[]
15
- sizes: number[]
16
- colors: { name: string; value: string }[]
17
- }
18
-
19
- export interface StorefrontState {
20
- heroProducts: CardProduct[]
21
- categories: ProductCategory2Category[]
22
- selectedProduct: StoreOverviewProduct | null
23
- lastClicked: CardProduct | null
24
- lastCategory: string | null
25
- bannerDismissed: boolean
26
- }
7
+ export * from './state.core'
27
8
 
28
- export const storefrontInitialState: StorefrontState = {
29
- heroProducts: [],
30
- categories: [],
31
- selectedProduct: null,
32
- lastClicked: null,
33
- lastCategory: null,
34
- bannerDismissed: false,
9
+ export const storefrontInitialState = {
10
+ ...storefrontCoreState,
35
11
  }
@@ -0,0 +1,161 @@
1
+ // Generated by Thor Commerce storefront (template: assets/templates/app/lib/__STOREFRONT__/store-context.ts)
2
+ //
3
+ // Host store context — the explicit single-store configuration the generated
4
+ // store runs on. It is resolved and validated from the host environment BEFORE
5
+ // any commerce call, so an incomplete or invalid configuration fails visibly
6
+ // instead of silently falling back to demo data.
7
+ //
8
+ // Boundary: this module is server-only and reads no Next.js or React API. It
9
+ // owns the environment contract; the commerce bridge owns the backend calls.
10
+ //
11
+ // The values are the multitenancy seam: a future tenancy layer varies
12
+ // storeId/channel/region/currency without redefining the commerce shapes. In
13
+ // real mode the currency is NEVER hardcoded — it comes from the host
14
+ // environment; mock mode resolves a labelled fixture context so the store runs
15
+ // with no environment configured at all.
16
+
17
+ import type { StoreContext } from '@thorprovider/types'
18
+ import { storeContextSchema } from '@thorprovider/runtime/contracts'
19
+
20
+ /** Data source selected explicitly by the host; never inferred from a failure. */
21
+ export type StoreDataSource = 'mock' | 'medusa'
22
+
23
+ /** Host environment variable names that make up the store context. */
24
+ export const STORE_CONTEXT_ENV = {
25
+ storeId: 'THOR_STORE_ID',
26
+ salesChannelId: 'MEDUSA_SALES_CHANNEL_ID',
27
+ regionId: 'MEDUSA_REGION_ID',
28
+ currencyCode: 'MEDUSA_CURRENCY_CODE',
29
+ } as const
30
+
31
+ /** Real-mode backend configuration required before any provider call. */
32
+ export const BACKEND_ENV = {
33
+ baseUrl: 'MEDUSA_BASE_URL',
34
+ publishableKey: 'MEDUSA_PUBLISHABLE_KEY',
35
+ } as const
36
+
37
+ /** Host environment variable that selects mock vs real mode. */
38
+ export const DATA_SOURCE_ENV = 'THOR_DATA_SOURCE'
39
+
40
+ /**
41
+ * Host environment variable with the canonical public origin of the store
42
+ * (used for `metadataBase`, `robots.txt`, and `sitemap.xml`). Optional: when
43
+ * unset or invalid no absolute URL is invented.
44
+ */
45
+ export const SITE_URL_ENV = 'THOR_SITE_URL'
46
+
47
+ /**
48
+ * Visible configuration failure. Thrown before commerce so a missing or invalid
49
+ * store context surfaces as an error instead of fictional data.
50
+ */
51
+ export class StoreConfigError extends Error {
52
+ readonly code = 'STOREFRONT_CONFIG_ERROR'
53
+ constructor(message: string) {
54
+ super(message)
55
+ this.name = 'StoreConfigError'
56
+ }
57
+ }
58
+
59
+ /**
60
+ * The explicitly selected data source. Defaults to `mock` (the safe, offline,
61
+ * explicitly labelled mode) so a store never silently talks to a half-configured
62
+ * backend; real mode must be selected on purpose.
63
+ */
64
+ export function getDataSource(): StoreDataSource {
65
+ return process.env[DATA_SOURCE_ENV] === 'medusa' ? 'medusa' : 'mock'
66
+ }
67
+
68
+ /** True when the host explicitly selected real (Medusa) mode. */
69
+ export function isRealMode(): boolean {
70
+ return getDataSource() === 'medusa'
71
+ }
72
+
73
+ /**
74
+ * Normalise a configured site URL to its origin (`https://example.com`), or
75
+ * `undefined` when it is absent, blank, not a valid URL, or not http(s). Pure
76
+ * and total: it never throws, so an invalid value degrades to "no site URL"
77
+ * instead of failing the build.
78
+ */
79
+ export function parseSiteUrl(value: string | undefined): string | undefined {
80
+ if (typeof value !== 'string' || value.trim() === '') return undefined
81
+ try {
82
+ const url = new URL(value)
83
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return undefined
84
+ return url.origin
85
+ } catch {
86
+ return undefined
87
+ }
88
+ }
89
+
90
+ /** The canonical public origin from the host environment, or `undefined`. */
91
+ export function getSiteUrl(): string | undefined {
92
+ return parseSiteUrl(process.env[SITE_URL_ENV])
93
+ }
94
+
95
+ /**
96
+ * Fixture context resolved in mock mode. It satisfies `storeContextSchema` and
97
+ * mirrors the values the bundled mock provider serves, so a store with no
98
+ * environment configured still has a valid context to render and operate its
99
+ * cart against. It is a labelled fixture, never a real backend identity.
100
+ */
101
+ export const MOCK_STORE_CONTEXT: StoreContext = {
102
+ storeId: 'mock-store',
103
+ salesChannelId: 'mock-sales-channel',
104
+ regionId: 'mock-region-us',
105
+ currencyCode: 'USD',
106
+ }
107
+
108
+ function requireEnv(name: string): string {
109
+ const value = process.env[name]
110
+ if (typeof value !== 'string' || value.trim() === '') {
111
+ throw new StoreConfigError(
112
+ `Missing required environment variable ${name}. Set it in the host environment before the store can resolve its context.`,
113
+ )
114
+ }
115
+ return value
116
+ }
117
+
118
+ /**
119
+ * Resolve and validate the single-store context.
120
+ *
121
+ * Mock mode returns the labelled fixture context and reads no environment
122
+ * variables, so a created store runs with no configuration. Real mode resolves
123
+ * the four variables from the host environment and validates them through the
124
+ * shared runtime schema, so an invalid context is rejected before any commerce
125
+ * call. Throws `StoreConfigError` naming the missing variable — never a secret
126
+ * value.
127
+ */
128
+ export function getStoreContext(): StoreContext {
129
+ if (!isRealMode()) return MOCK_STORE_CONTEXT
130
+ const candidate = {
131
+ storeId: requireEnv(STORE_CONTEXT_ENV.storeId),
132
+ salesChannelId: requireEnv(STORE_CONTEXT_ENV.salesChannelId),
133
+ regionId: requireEnv(STORE_CONTEXT_ENV.regionId),
134
+ currencyCode: requireEnv(STORE_CONTEXT_ENV.currencyCode),
135
+ }
136
+ const parsed = storeContextSchema.safeParse(candidate)
137
+ if (!parsed.success) {
138
+ const issues = parsed.error.issues
139
+ .map((issue) => `${issue.path.join('.') || 'context'}: ${issue.message}`)
140
+ .join('; ')
141
+ throw new StoreConfigError(`Invalid store context: ${issues}`)
142
+ }
143
+ return parsed.data
144
+ }
145
+
146
+ /**
147
+ * Assert that real-mode backend configuration is present before any provider
148
+ * call. Mock mode needs no backend. Throws `StoreConfigError` (never printing a
149
+ * key value) so a misconfigured real store fails visibly.
150
+ */
151
+ export function assertBackendConfigured(source: StoreDataSource = getDataSource()): void {
152
+ if (source !== 'medusa') return
153
+ const missing: string[] = []
154
+ if (!process.env[BACKEND_ENV.baseUrl]?.trim()) missing.push(BACKEND_ENV.baseUrl)
155
+ if (!process.env[BACKEND_ENV.publishableKey]?.trim()) missing.push(BACKEND_ENV.publishableKey)
156
+ if (missing.length > 0) {
157
+ throw new StoreConfigError(
158
+ `THOR_DATA_SOURCE=medusa requires ${missing.join(', ')}. Configure the backend or select THOR_DATA_SOURCE=mock explicitly.`,
159
+ )
160
+ }
161
+ }
@@ -0,0 +1,25 @@
1
+ 'use client'
2
+
3
+ // Optional section fragment: cta (mirrors recipes/sections.json) — client registry.
4
+ // The matching server-side catalog lives in cta.tsx. @thorprovider/blocks ships a
5
+ // 'use client' boundary, but this fragment keeps the explicit client boundary so
6
+ // the section composes identically in any consumer graph.
7
+
8
+ import { CtaSection1 as CtaBlock } from '@thorprovider/blocks'
9
+ import { ctaSection1PropsSchema } from './schema.cta'
10
+ import type { ViewRegistry } from '../types'
11
+
12
+ export const ctaRegistry: ViewRegistry = {
13
+ Cta: ({ props }) => {
14
+ const parsed = ctaSection1PropsSchema.parse(props)
15
+ return (
16
+ <CtaBlock
17
+ badgeText={parsed.badgeText}
18
+ title={parsed.title}
19
+ description={parsed.description}
20
+ primaryCta={parsed.primaryCta}
21
+ secondaryCta={parsed.secondaryCta}
22
+ />
23
+ )
24
+ },
25
+ }
@@ -0,0 +1,16 @@
1
+ // Optional section fragment: cta (mirrors recipes/sections.json)
2
+ // Server-side part: element type + zod catalog entry. The component mappings
3
+ // live in cta.client.tsx ('use client') and are composed via composeSections.
4
+
5
+ import { ctaSection1PropsSchema } from './schema.cta'
6
+
7
+ export const ctaSection = {
8
+ elementType: 'Cta',
9
+ catalog: {
10
+ Cta: {
11
+ props: ctaSection1PropsSchema,
12
+ description: 'Call-to-action banner with optional primary and secondary links.',
13
+ },
14
+ },
15
+ registryModule: './cta.client',
16
+ }
@@ -0,0 +1,24 @@
1
+ 'use client'
2
+
3
+ // Optional section fragment: faq (mirrors recipes/sections.json) — client registry.
4
+ // The matching server-side catalog lives in faq.tsx. @thorprovider/blocks ships a
5
+ // 'use client' boundary, but this fragment keeps the explicit client boundary so
6
+ // the section composes identically in any consumer graph.
7
+
8
+ import { FaqBlock1 as FaqBlock } from '@thorprovider/blocks'
9
+ import { faqPropsSchema } from './schema.faq'
10
+ import type { ViewRegistry } from '../types'
11
+
12
+ export const faqRegistry: ViewRegistry = {
13
+ Faq: ({ props }) => {
14
+ const parsed = faqPropsSchema.parse(props)
15
+ return (
16
+ <FaqBlock
17
+ title={parsed.title ?? 'Preguntas frecuentes'}
18
+ description={parsed.description}
19
+ items={parsed.items}
20
+ accordionType={parsed.accordionType ?? 'single'}
21
+ />
22
+ )
23
+ },
24
+ }
@@ -1,29 +1,16 @@
1
1
  // Optional section fragment: faq (mirrors recipes/sections.json)
2
+ // Server-side part: element type + zod catalog entry. The component mappings
3
+ // live in faq.client.tsx ('use client') and are composed via composeSections.
2
4
 
3
- import { FaqBlock1 as FaqBlock } from '@thorprovider/blocks'
4
- import { z } from 'zod'
5
+ import { faqPropsSchema } from './schema.faq'
5
6
 
6
7
  export const faqSection = {
7
8
  elementType: 'Faq',
8
9
  catalog: {
9
10
  Faq: {
10
- props: z.object({
11
- title: z.string().optional(),
12
- description: z.string().optional(),
13
- items: z.array(z.record(z.string(), z.unknown())),
14
- accordionType: z.enum(['single', 'multiple']).optional(),
15
- }),
11
+ props: faqPropsSchema,
16
12
  description: 'Frequently asked questions accordion.',
17
13
  },
18
14
  },
19
- registry: {
20
- Faq: ({ props }: { props: Record<string, unknown> }) => (
21
- <FaqBlock
22
- title={(props.title as string) ?? 'Preguntas frecuentes'}
23
- description={props.description as string}
24
- items={props.items as never}
25
- accordionType={(props.accordionType as 'single' | 'multiple') ?? 'single'}
26
- />
27
- ),
28
- },
15
+ registryModule: './faq.client',
29
16
  }
@@ -0,0 +1,18 @@
1
+ // Section catalog composition. Each section fragment is a server-side part
2
+ // (element type + zod catalog entry); this module merges their catalogs the same
3
+ // way `catalog.ts` merges view catalogs, so the generated storefront catalog can
4
+ // advertise the editorial element types. A fragment's client registry is
5
+ // addressed by its `registryModule` and never imported here, so the server
6
+ // catalog never pulls the React graph.
7
+
8
+ import type { ViewCatalogEntry } from '../types'
9
+ import { ctaSection } from './cta'
10
+ import { faqSection } from './faq'
11
+ import { productsSection } from './products'
12
+
13
+ export const sectionModules = [faqSection, ctaSection, productsSection] as const
14
+
15
+ export const sectionCatalog: Record<string, ViewCatalogEntry> = Object.assign(
16
+ {},
17
+ ...sectionModules.map((section) => section.catalog),
18
+ )
@@ -3,27 +3,39 @@
3
3
  import { LogoCloud1 as LogoCloudBlock } from '@thorprovider/blocks'
4
4
  import { z } from 'zod'
5
5
 
6
+ // Mirrors the LogoCloud1Platform/LogoCloud1Props block contracts. `id` is a
7
+ // simple-icons slug resolved internally by the block.
8
+ const logoCloudPlatformSchema = z.object({
9
+ name: z.string(),
10
+ id: z.string(),
11
+ })
12
+
13
+ const logoCloudPropsSchema = z.object({
14
+ title: z.string().optional(),
15
+ description: z.string().optional(),
16
+ moreLabel: z.string().optional(),
17
+ platforms: z.array(logoCloudPlatformSchema),
18
+ })
19
+
6
20
  export const logoCloudSection = {
7
21
  elementType: 'LogoCloud',
8
22
  catalog: {
9
23
  LogoCloud: {
10
- props: z.object({
11
- title: z.string().optional(),
12
- description: z.string().optional(),
13
- moreLabel: z.string().optional(),
14
- platforms: z.array(z.record(z.string(), z.unknown())),
15
- }),
24
+ props: logoCloudPropsSchema,
16
25
  description: 'Brand or partner logo strip.',
17
26
  },
18
27
  },
19
28
  registry: {
20
- LogoCloud: ({ props }: { props: Record<string, unknown> }) => (
21
- <LogoCloudBlock
22
- title={(props.title as string) ?? 'Marcas que confían en nosotros'}
23
- description={props.description as string}
24
- moreLabel={props.moreLabel as string}
25
- platforms={props.platforms as never}
26
- />
27
- ),
29
+ LogoCloud: ({ props }: { props: Record<string, unknown> }) => {
30
+ const parsed = logoCloudPropsSchema.parse(props)
31
+ return (
32
+ <LogoCloudBlock
33
+ title={parsed.title ?? 'Marcas que confían en nosotros'}
34
+ description={parsed.description}
35
+ moreLabel={parsed.moreLabel}
36
+ platforms={parsed.platforms}
37
+ />
38
+ )
39
+ },
28
40
  },
29
41
  }