@thorprovider/storefront 0.3.1 → 0.4.0-mvp.0

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 (179) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +248 -99
  3. package/assets/templates/app/app/[[...slug]]/page.tsx +13 -2
  4. package/assets/templates/app/app/layout.tsx +18 -4
  5. package/assets/templates/app/app/robots.ts +13 -0
  6. package/assets/templates/app/app/sitemap.ts +12 -0
  7. package/assets/templates/app/lib/__STOREFRONT__/actions.ts +542 -0
  8. package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
  9. package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +40 -57
  10. package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +608 -0
  11. package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +165 -0
  12. package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
  13. package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
  14. package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
  15. package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +232 -51
  16. package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +3 -3
  17. package/assets/templates/app/lib/__STOREFRONT__/schema.ts +121 -0
  18. package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
  19. package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
  20. package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +96 -0
  21. package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +34 -0
  22. package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +100 -11
  23. package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
  24. package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +26 -0
  25. package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
  26. package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
  27. package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +142 -0
  28. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
  29. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
  30. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
  31. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
  32. package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +17 -0
  33. package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
  34. package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
  35. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
  36. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
  37. package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
  38. package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
  39. package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
  40. package/assets/templates/features/checkout/handlers.ts +153 -0
  41. package/assets/templates/features/checkout/state.ts +61 -0
  42. package/assets/templates/features/customer-auth/handlers.ts +59 -0
  43. package/assets/templates/features/customer-auth/state.ts +36 -0
  44. package/assets/templates/features/recommendations/state.ts +11 -0
  45. package/assets/templates/views/account/content.ts +35 -0
  46. package/assets/templates/views/account/index.tsx +114 -0
  47. package/assets/templates/views/account/registry.tsx +378 -0
  48. package/assets/templates/views/account/schema.ts +52 -0
  49. package/assets/templates/views/category/assembly/assembly.json +28 -0
  50. package/assets/templates/views/category/assembly/index.skeleton.tsx +193 -0
  51. package/assets/templates/views/category/assembly/registry.skeleton.tsx +33 -0
  52. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
  53. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
  54. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
  55. package/assets/templates/views/category/content.ts +30 -0
  56. package/assets/templates/views/category/data.ts +12 -0
  57. package/assets/templates/views/category/index.tsx +230 -0
  58. package/assets/templates/views/category/props.ts +16 -0
  59. package/assets/templates/views/category/registry.tsx +103 -0
  60. package/assets/templates/views/category/schema.ts +78 -0
  61. package/assets/templates/views/checkout/content.ts +105 -0
  62. package/assets/templates/views/checkout/data.ts +23 -0
  63. package/assets/templates/views/checkout/index.tsx +111 -0
  64. package/assets/templates/views/checkout/props.ts +49 -0
  65. package/assets/templates/views/checkout/registry.tsx +217 -0
  66. package/assets/templates/views/checkout/schema.ts +229 -0
  67. package/assets/templates/views/home/assembly/assembly.json +24 -0
  68. package/assets/templates/views/home/assembly/index.skeleton.tsx +72 -0
  69. package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
  70. package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
  71. package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
  72. package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
  73. package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
  74. package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
  75. package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
  76. package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
  77. package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
  78. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
  79. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
  80. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
  81. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
  82. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
  83. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
  84. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
  85. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
  86. package/assets/templates/views/home/content.ts +70 -0
  87. package/assets/templates/views/home/data.ts +15 -0
  88. package/assets/templates/views/home/index.tsx +38 -84
  89. package/assets/templates/views/home/props.ts +22 -0
  90. package/assets/templates/views/home/registry.tsx +38 -22
  91. package/assets/templates/views/home/schema.ts +93 -0
  92. package/assets/templates/views/product/content.ts +25 -0
  93. package/assets/templates/views/product/data.ts +48 -0
  94. package/assets/templates/views/product/index.tsx +104 -0
  95. package/assets/templates/views/product/props.ts +20 -0
  96. package/assets/templates/views/product/registry.tsx +78 -0
  97. package/assets/templates/views/product/schema.ts +108 -0
  98. package/assets/templates/views/static/content.ts +99 -0
  99. package/assets/templates/views/static/index.tsx +60 -0
  100. package/assets/templates/views/static/registry.tsx +66 -0
  101. package/assets/templates/views/static/schema.ts +45 -0
  102. package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/index.tsx +8 -70
  103. package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/registry.tsx +23 -18
  104. package/assets/templates/views/user-panel/schema.ts +79 -0
  105. package/bin/storefront.js +1278 -79
  106. package/commands/sf-add-view.md +44 -13
  107. package/commands/sf-feature.md +28 -0
  108. package/commands/sf-theme.md +14 -8
  109. package/commands/sf-view.md +54 -12
  110. package/package.json +6 -4
  111. package/recipes/archetypes.json +28 -1
  112. package/recipes/catalog.json +361 -0
  113. package/recipes/catalog.mjs +128 -0
  114. package/recipes/catalog.schema.json +64 -0
  115. package/recipes/feature.schema.json +89 -0
  116. package/recipes/features.json +139 -0
  117. package/recipes/manifest.schema.json +69 -0
  118. package/recipes/operations.schema.json +43 -0
  119. package/recipes/recipe.schema.json +11 -2
  120. package/recipes/recipes.json +60 -5
  121. package/recipes/sections.json +24 -5
  122. package/recipes/validate.mjs +784 -32
  123. package/scripts/candidate.mjs +236 -0
  124. package/scripts/composers.mjs +140 -0
  125. package/scripts/file-classes.mjs +116 -0
  126. package/scripts/logical-controls.mjs +61 -0
  127. package/scripts/manifest.mjs +298 -0
  128. package/scripts/operation-registry.mjs +256 -0
  129. package/scripts/operations.mjs +720 -0
  130. package/scripts/ownership-conflict.mjs +356 -0
  131. package/scripts/pinning.mjs +504 -0
  132. package/scripts/plan-apply.mjs +843 -0
  133. package/scripts/preflight.mjs +51 -0
  134. package/scripts/reconcile.mjs +511 -0
  135. package/scripts/render-templates.mjs +245 -28
  136. package/scripts/schema-validate.mjs +132 -0
  137. package/scripts/section-compat.mjs +300 -0
  138. package/scripts/section-operations.mjs +508 -0
  139. package/scripts/sync-assets.mjs +22 -15
  140. package/scripts/view-assembly.mjs +291 -0
  141. package/scripts/view-selection.mjs +211 -0
  142. package/skills/sf-feature/SKILL.md +67 -0
  143. package/skills/sf-theme-gen/SKILL.md +12 -5
  144. package/skills/sf-view-gen/SKILL.md +123 -93
  145. package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -1
  146. package/skills/sf-view-gen/assets/recipes/catalog.json +361 -0
  147. package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
  148. package/skills/sf-view-gen/assets/recipes/features.json +139 -0
  149. package/skills/sf-view-gen/assets/recipes/recipes.json +60 -5
  150. package/skills/sf-view-gen/assets/recipes/sections.json +24 -5
  151. package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
  152. package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
  153. package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
  154. package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
  155. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
  156. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
  157. package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
  158. package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
  159. package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
  160. package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
  161. package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
  162. package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
  163. package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
  164. package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
  165. package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
  166. package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
  167. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
  168. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
  169. package/skills/sf-view-gen/references/views.md +44 -10
  170. package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
  171. package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
  172. package/skills/sf-view-gen/scripts/validate.mjs +784 -32
  173. package/skills/storefront-workflow/SKILL.md +116 -0
  174. package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
  175. package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
  176. package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
  177. package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
  178. package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
  179. package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
@@ -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,142 @@
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. The
13
+ // currency is NEVER hardcoded — it always comes from the host environment.
14
+
15
+ import type { StoreContext } from '@thorprovider/types'
16
+ import { storeContextSchema } from '@thorprovider/runtime/contracts'
17
+
18
+ /** Data source selected explicitly by the host; never inferred from a failure. */
19
+ export type StoreDataSource = 'mock' | 'medusa'
20
+
21
+ /** Host environment variable names that make up the store context. */
22
+ export const STORE_CONTEXT_ENV = {
23
+ storeId: 'THOR_STORE_ID',
24
+ salesChannelId: 'MEDUSA_SALES_CHANNEL_ID',
25
+ regionId: 'MEDUSA_REGION_ID',
26
+ currencyCode: 'THOR_CURRENCY_CODE',
27
+ } as const
28
+
29
+ /** Real-mode backend configuration required before any provider call. */
30
+ export const BACKEND_ENV = {
31
+ baseUrl: 'MEDUSA_BASE_URL',
32
+ publishableKey: 'MEDUSA_PUBLISHABLE_KEY',
33
+ } as const
34
+
35
+ /** Host environment variable that selects mock vs real mode. */
36
+ export const DATA_SOURCE_ENV = 'THOR_DATA_SOURCE'
37
+
38
+ /**
39
+ * Host environment variable with the canonical public origin of the store
40
+ * (used for `metadataBase`, `robots.txt`, and `sitemap.xml`). Optional: when
41
+ * unset or invalid no absolute URL is invented.
42
+ */
43
+ export const SITE_URL_ENV = 'THOR_SITE_URL'
44
+
45
+ /**
46
+ * Visible configuration failure. Thrown before commerce so a missing or invalid
47
+ * store context surfaces as an error instead of fictional data.
48
+ */
49
+ export class StoreConfigError extends Error {
50
+ readonly code = 'STOREFRONT_CONFIG_ERROR'
51
+ constructor(message: string) {
52
+ super(message)
53
+ this.name = 'StoreConfigError'
54
+ }
55
+ }
56
+
57
+ /**
58
+ * The explicitly selected data source. Defaults to `mock` (the safe, offline,
59
+ * explicitly labelled mode) so a store never silently talks to a half-configured
60
+ * backend; real mode must be selected on purpose.
61
+ */
62
+ export function getDataSource(): StoreDataSource {
63
+ return process.env[DATA_SOURCE_ENV] === 'medusa' ? 'medusa' : 'mock'
64
+ }
65
+
66
+ /** True when the host explicitly selected real (Medusa) mode. */
67
+ export function isRealMode(): boolean {
68
+ return getDataSource() === 'medusa'
69
+ }
70
+
71
+ /**
72
+ * Normalise a configured site URL to its origin (`https://example.com`), or
73
+ * `undefined` when it is absent, blank, not a valid URL, or not http(s). Pure
74
+ * and total: it never throws, so an invalid value degrades to "no site URL"
75
+ * instead of failing the build.
76
+ */
77
+ export function parseSiteUrl(value: string | undefined): string | undefined {
78
+ if (typeof value !== 'string' || value.trim() === '') return undefined
79
+ try {
80
+ const url = new URL(value)
81
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return undefined
82
+ return url.origin
83
+ } catch {
84
+ return undefined
85
+ }
86
+ }
87
+
88
+ /** The canonical public origin from the host environment, or `undefined`. */
89
+ export function getSiteUrl(): string | undefined {
90
+ return parseSiteUrl(process.env[SITE_URL_ENV])
91
+ }
92
+
93
+ function requireEnv(name: string): string {
94
+ const value = process.env[name]
95
+ if (typeof value !== 'string' || value.trim() === '') {
96
+ throw new StoreConfigError(
97
+ `Missing required environment variable ${name}. Set it in the host environment before the store can resolve its context.`,
98
+ )
99
+ }
100
+ return value
101
+ }
102
+
103
+ /**
104
+ * Resolve and validate the single-store context from the host environment.
105
+ *
106
+ * Validated through the shared runtime schema, so an invalid context is rejected
107
+ * before any commerce call. Throws `StoreConfigError` naming the missing
108
+ * variable — never a secret value.
109
+ */
110
+ export function getStoreContext(): StoreContext {
111
+ const candidate = {
112
+ storeId: requireEnv(STORE_CONTEXT_ENV.storeId),
113
+ salesChannelId: requireEnv(STORE_CONTEXT_ENV.salesChannelId),
114
+ regionId: requireEnv(STORE_CONTEXT_ENV.regionId),
115
+ currencyCode: requireEnv(STORE_CONTEXT_ENV.currencyCode),
116
+ }
117
+ const parsed = storeContextSchema.safeParse(candidate)
118
+ if (!parsed.success) {
119
+ const issues = parsed.error.issues
120
+ .map((issue) => `${issue.path.join('.') || 'context'}: ${issue.message}`)
121
+ .join('; ')
122
+ throw new StoreConfigError(`Invalid store context: ${issues}`)
123
+ }
124
+ return parsed.data
125
+ }
126
+
127
+ /**
128
+ * Assert that real-mode backend configuration is present before any provider
129
+ * call. Mock mode needs no backend. Throws `StoreConfigError` (never printing a
130
+ * key value) so a misconfigured real store fails visibly.
131
+ */
132
+ export function assertBackendConfigured(source: StoreDataSource = getDataSource()): void {
133
+ if (source !== 'medusa') return
134
+ const missing: string[] = []
135
+ if (!process.env[BACKEND_ENV.baseUrl]?.trim()) missing.push(BACKEND_ENV.baseUrl)
136
+ if (!process.env[BACKEND_ENV.publishableKey]?.trim()) missing.push(BACKEND_ENV.publishableKey)
137
+ if (missing.length > 0) {
138
+ throw new StoreConfigError(
139
+ `THOR_DATA_SOURCE=medusa requires ${missing.join(', ')}. Configure the backend or select THOR_DATA_SOURCE=mock explicitly.`,
140
+ )
141
+ }
142
+ }
@@ -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,17 @@
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
+
12
+ export const sectionModules = [faqSection, ctaSection] as const
13
+
14
+ export const sectionCatalog: Record<string, ViewCatalogEntry> = Object.assign(
15
+ {},
16
+ ...sectionModules.map((section) => section.catalog),
17
+ )
@@ -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
  }
@@ -3,31 +3,49 @@
3
3
  import { PricingSection1 as PricingBlock } from '@thorprovider/blocks'
4
4
  import { z } from 'zod'
5
5
 
6
+ // Mirrors the PricingTier/PricingSection1Props block contracts.
7
+ const pricingTierSchema = z.object({
8
+ id: z.string(),
9
+ name: z.string(),
10
+ description: z.string(),
11
+ // Precio sin símbolo de moneda; el símbolo se pasa aparte en `currencySymbol`.
12
+ price: z.string(),
13
+ // Frecuencia de facturación, p. ej. '/mes'.
14
+ frequency: z.string(),
15
+ features: z.array(z.string()),
16
+ popular: z.boolean().optional(),
17
+ })
18
+
19
+ const pricingPropsSchema = z.object({
20
+ sectionTitle: z.string().optional(),
21
+ sectionLabel: z.string().optional(),
22
+ currencySymbol: z.string().optional(),
23
+ popularBadgeLabel: z.string().optional(),
24
+ getStartedLabel: z.string().optional(),
25
+ tiers: z.array(pricingTierSchema),
26
+ })
27
+
6
28
  export const pricingSection = {
7
29
  elementType: 'Pricing',
8
30
  catalog: {
9
31
  Pricing: {
10
- props: z.object({
11
- sectionTitle: z.string().optional(),
12
- sectionLabel: z.string().optional(),
13
- currencySymbol: z.string().optional(),
14
- popularBadgeLabel: z.string().optional(),
15
- getStartedLabel: z.string().optional(),
16
- tiers: z.array(z.record(z.string(), z.unknown())),
17
- }),
32
+ props: pricingPropsSchema,
18
33
  description: 'Pricing tiers or plans.',
19
34
  },
20
35
  },
21
36
  registry: {
22
- Pricing: ({ props }: { props: Record<string, unknown> }) => (
23
- <PricingBlock
24
- sectionTitle={(props.sectionTitle as string) ?? 'Planes y precios'}
25
- sectionLabel={props.sectionLabel as string}
26
- currencySymbol={props.currencySymbol as string}
27
- popularBadgeLabel={(props.popularBadgeLabel as string) ?? 'Popular'}
28
- getStartedLabel={(props.getStartedLabel as string) ?? 'Empezar'}
29
- tiers={props.tiers as never}
30
- />
31
- ),
37
+ Pricing: ({ props }: { props: Record<string, unknown> }) => {
38
+ const parsed = pricingPropsSchema.parse(props)
39
+ return (
40
+ <PricingBlock
41
+ sectionTitle={parsed.sectionTitle ?? 'Planes y precios'}
42
+ sectionLabel={parsed.sectionLabel}
43
+ currencySymbol={parsed.currencySymbol}
44
+ popularBadgeLabel={parsed.popularBadgeLabel ?? 'Popular'}
45
+ getStartedLabel={parsed.getStartedLabel ?? 'Empezar'}
46
+ tiers={parsed.tiers}
47
+ />
48
+ )
49
+ },
32
50
  },
33
51
  }