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