@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,439 @@
1
+ 'use client'
2
+
3
+ // Thor Commerce view registry fragment: account
4
+ // Client boundary for the account view. Imports value bindings from
5
+ // @json-render/react and components from @thorprovider/blocks — both must stay
6
+ // on the client side of the RSC graph. Kept separate from index.tsx so the view
7
+ // spec (elements/catalog/route/loader) stays a pure Server Component module.
8
+ //
9
+ // Security invariant: the block-owned credential payload (email/password) is
10
+ // passed straight from the block callback to the Server Action and is NEVER
11
+ // written to the json-render state store, so no password reaches client state,
12
+ // HTML, or logs. Only the non-secret result (customer/error) is stored.
13
+
14
+ import { useEffect, useRef } from 'react'
15
+ import {
16
+ Button,
17
+ LoginPage1 as LoginBlock,
18
+ OrderHistory2,
19
+ SignUpPage1 as SignUpBlock,
20
+ } from '@thorprovider/blocks'
21
+ import { useStateStore } from '@json-render/react'
22
+ import type { Customer } from '@thorprovider/types'
23
+ import {
24
+ loadAccountAction,
25
+ loadOrdersPageAction,
26
+ loginAction,
27
+ logoutAction,
28
+ registerAction,
29
+ } from '../../actions'
30
+ import type { AccountOrderView } from '../../state.core'
31
+ import type { ViewRegistry } from '../types'
32
+ import {
33
+ accountLoginPropsSchema,
34
+ accountOrdersPropsSchema,
35
+ accountRegisterPropsSchema,
36
+ } from './schema'
37
+
38
+ // The structured result the auth Server Actions return (see ../../actions).
39
+ type AuthResult = { ok: true; customer: Customer | null } | { ok: false; error: string }
40
+ type SetState = (path: string, value: unknown) => void
41
+
42
+ /** Bounded page size for the account order-history window. */
43
+ const ORDERS_PAGE_SIZE = 10
44
+
45
+ // Apply a settled auth result truthfully: a failure sets an error and NEVER a
46
+ // logged-in state; a success stores the customer (or null) and nothing else.
47
+ function applyAuthResult(set: SetState, result: AuthResult): void {
48
+ set('/accountPending', false)
49
+ if (result.ok) {
50
+ set('/accountCustomer', result.customer)
51
+ set('/accountAuthenticated', result.customer !== null)
52
+ set('/accountError', null)
53
+ set('/accountMode', 'login')
54
+ return
55
+ }
56
+ set('/accountError', result.error)
57
+ }
58
+
59
+ // A rejected Server Action (network/RPC) must clear pending and surface a
60
+ // message; never swallow it. Only a non-secret context line is logged.
61
+ function authFailure(set: SetState, scope: string) {
62
+ return (error: unknown) => {
63
+ console.error(
64
+ `[storefront/account] ${scope} failed`,
65
+ error instanceof Error ? error.message : error,
66
+ )
67
+ set('/accountPending', false)
68
+ set('/accountError', 'No se pudo completar la operación. Inténtalo de nuevo.')
69
+ }
70
+ }
71
+
72
+ // Non-secret status surface for the block pages, which expose no error/pending
73
+ // prop of their own. Renders nothing when there is no pending work or error.
74
+ function AuthNotice({ pending, error }: { pending: boolean; error: string | null }) {
75
+ if (!pending && !error) return null
76
+ return (
77
+ <div
78
+ role="alert"
79
+ aria-live="polite"
80
+ className="bg-background text-foreground fixed inset-x-0 top-4 z-50 mx-auto w-fit max-w-md rounded-md border px-4 py-2 text-center text-sm shadow-lg"
81
+ >
82
+ {pending ? 'Procesando…' : error}
83
+ </div>
84
+ )
85
+ }
86
+
87
+ // The OrderHistory2 block's order shape (see @thorprovider/blocks). Kept local
88
+ // so the registry maps the account order rows to the block contract without a
89
+ // shared dependency on block internals.
90
+ interface BlockOrderItem {
91
+ id: string
92
+ name: string
93
+ image: string
94
+ details: string
95
+ price: string
96
+ quantity: number
97
+ }
98
+ interface BlockOrder {
99
+ id: string
100
+ date: string
101
+ status: string
102
+ total: string
103
+ items: BlockOrderItem[]
104
+ }
105
+
106
+ // Map a backend-sourced account order row to the block's order shape. The block
107
+ // has a single `id` slot which it also renders ("Pedido {id}"), so it carries the
108
+ // display number; the registry keeps a number→backend-id map so selection is
109
+ // keyed by the REAL backend order id. An absent variant/image becomes '' because
110
+ // the block contract is a string — never an invented value.
111
+ function toBlockOrder(order: AccountOrderView): BlockOrder {
112
+ return {
113
+ id: order.number,
114
+ date: order.date,
115
+ status: order.status,
116
+ total: order.total,
117
+ items: order.items.map((item) => ({
118
+ id: item.id,
119
+ name: item.name,
120
+ image: item.image ?? '',
121
+ details: item.variant ?? '',
122
+ price: item.lineTotal,
123
+ quantity: item.quantity,
124
+ })),
125
+ }
126
+ }
127
+
128
+ // Inline order detail assembled from the already-fetched list row (no second
129
+ // backend request). Only fields the backend supplied are shown.
130
+ function OrderDetail({ order }: { order: AccountOrderView }) {
131
+ const totals = (
132
+ [
133
+ { label: 'Subtotal', value: order.subtotal },
134
+ { label: 'Impuestos', value: order.tax },
135
+ { label: 'Envío', value: order.shipping },
136
+ { label: 'Total', value: order.total },
137
+ ] as { label: string; value?: string }[]
138
+ ).filter((row): row is { label: string; value: string } => row.value !== undefined)
139
+ return (
140
+ <div className="mx-auto w-full max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
141
+ <div className="rounded-lg border p-6">
142
+ <h3 className="text-lg font-semibold">Pedido {order.number}</h3>
143
+ <p className="text-muted-foreground mt-1 text-sm">
144
+ {order.date} · {order.status}
145
+ </p>
146
+ {order.address ? (
147
+ <p className="text-muted-foreground mt-2 text-sm">{order.address}</p>
148
+ ) : null}
149
+ <ul className="mt-4 divide-y">
150
+ {order.items.map((item) => (
151
+ <li key={item.id} className="flex items-center justify-between py-2 text-sm">
152
+ <span>
153
+ {item.name}
154
+ {item.variant ? (
155
+ <span className="text-muted-foreground"> · {item.variant}</span>
156
+ ) : null}
157
+ <span className="text-muted-foreground"> × {item.quantity}</span>
158
+ </span>
159
+ <span>{item.lineTotal}</span>
160
+ </li>
161
+ ))}
162
+ </ul>
163
+ <dl className="mt-4 space-y-1 border-t pt-4 text-sm">
164
+ {totals.map((row) => (
165
+ <div key={row.label} className="flex justify-between">
166
+ <dt className="text-muted-foreground">{row.label}</dt>
167
+ <dd>{row.value}</dd>
168
+ </div>
169
+ ))}
170
+ </dl>
171
+ </div>
172
+ </div>
173
+ )
174
+ }
175
+
176
+ export const registry: ViewRegistry = {
177
+ /**
178
+ * Headless bootstrap rendered on the account route. It resolves the current
179
+ * customer from the cookie-authorized Server Action once, so a reload keeps
180
+ * the signed-in state without re-entering credentials. It never reads a token.
181
+ */
182
+ AccountBoot: () => {
183
+ const { set } = useStateStore()
184
+ const booted = useRef(false)
185
+ useEffect(() => {
186
+ if (booted.current) return
187
+ booted.current = true
188
+ void loadAccountAction()
189
+ .then((result) => {
190
+ if (!result.ok) {
191
+ set('/accountCustomer', null)
192
+ set('/accountAuthenticated', false)
193
+ set('/accountError', result.error)
194
+ return
195
+ }
196
+ set('/accountCustomer', result.customer)
197
+ set('/accountAuthenticated', result.customer !== null)
198
+ set('/accountError', null)
199
+ })
200
+ .catch((error) => {
201
+ console.error(
202
+ '[storefront/account] loadAccount failed',
203
+ error instanceof Error ? error.message : error,
204
+ )
205
+ set('/accountCustomer', null)
206
+ set('/accountAuthenticated', false)
207
+ set('/accountError', 'No se pudo cargar la cuenta. Inténtalo de nuevo.')
208
+ })
209
+ }, [])
210
+ return null
211
+ },
212
+ AccountLogin: ({ props, emit }) => {
213
+ const { state, set } = useStateStore()
214
+ const parsed = accountLoginPropsSchema.parse(props)
215
+ const error = typeof state.accountError === 'string' ? state.accountError : null
216
+ const pending = Boolean(state.accountPending)
217
+ return (
218
+ <>
219
+ <AuthNotice pending={pending} error={error} />
220
+ <LoginBlock
221
+ logoText={parsed.logoText}
222
+ welcomeTitleLabel={parsed.welcomeTitleLabel}
223
+ welcomeDescriptionLabel={parsed.welcomeDescriptionLabel}
224
+ emailLabel={parsed.emailLabel}
225
+ passwordLabel={parsed.passwordLabel}
226
+ signInLabel={parsed.signInLabel}
227
+ createAccountLabel={parsed.createAccountLabel}
228
+ // The credentials stay in the block's local state and go straight to
229
+ // the Server Action; they are never written to the store.
230
+ onSubmit={(data) => {
231
+ set('/accountPending', true)
232
+ set('/accountError', null)
233
+ void loginAction({ email: data.email, password: data.password })
234
+ .then((result) => applyAuthResult(set, result))
235
+ .catch(authFailure(set, 'login'))
236
+ }}
237
+ onCreateAccount={() => emit('showRegister')}
238
+ />
239
+ </>
240
+ )
241
+ },
242
+ AccountRegister: ({ props, emit }) => {
243
+ const { state, set } = useStateStore()
244
+ const parsed = accountRegisterPropsSchema.parse(props)
245
+ const error = typeof state.accountError === 'string' ? state.accountError : null
246
+ const pending = Boolean(state.accountPending)
247
+ return (
248
+ <>
249
+ <AuthNotice pending={pending} error={error} />
250
+ <SignUpBlock
251
+ logoText={parsed.logoText}
252
+ createAccountTitleLabel={parsed.createAccountTitleLabel}
253
+ createAccountDescriptionLabel={parsed.createAccountDescriptionLabel}
254
+ fullNameLabel={parsed.fullNameLabel}
255
+ emailLabel={parsed.emailLabel}
256
+ passwordLabel={parsed.passwordLabel}
257
+ createAccountLabel={parsed.createAccountLabel}
258
+ signInLabel={parsed.signInLabel}
259
+ onSubmit={(data) => {
260
+ const parts = data.fullName.trim().split(/\s+/).filter(Boolean)
261
+ set('/accountPending', true)
262
+ set('/accountError', null)
263
+ void registerAction({
264
+ email: data.email,
265
+ password: data.password,
266
+ first_name: parts[0] ?? '',
267
+ last_name: parts.slice(1).join(' '),
268
+ })
269
+ .then((result) => applyAuthResult(set, result))
270
+ .catch(authFailure(set, 'register'))
271
+ }}
272
+ onSignIn={() => emit('showLogin')}
273
+ />
274
+ </>
275
+ )
276
+ },
277
+ AccountPanel: ({ emit }) => {
278
+ const { state } = useStateStore()
279
+ const customer = (state.accountCustomer ?? null) as Customer | null
280
+ const error = typeof state.accountError === 'string' ? state.accountError : null
281
+ const pending = Boolean(state.accountPending)
282
+ const name = customer
283
+ ? [customer.firstName, customer.lastName].filter(Boolean).join(' ') || customer.email
284
+ : ''
285
+ return (
286
+ <div className="mx-auto max-w-md px-4 py-16 text-center">
287
+ <h1 className="text-2xl font-bold">Mi cuenta</h1>
288
+ <p className="text-muted-foreground mt-2 text-sm">Has iniciado sesión como</p>
289
+ <p className="mt-1 text-lg font-medium">{name}</p>
290
+ {customer?.email ? <p className="text-muted-foreground text-sm">{customer.email}</p> : null}
291
+ {error ? (
292
+ <p role="alert" className="text-destructive mt-4 text-sm">
293
+ {error}
294
+ </p>
295
+ ) : null}
296
+ <Button className="mt-6" disabled={pending} onClick={() => emit('logout')}>
297
+ {pending ? 'Cerrando sesión…' : 'Cerrar sesión'}
298
+ </Button>
299
+ </div>
300
+ )
301
+ },
302
+ /**
303
+ * Signed-in order history. The first page loads once from the cookie-authorized
304
+ * Server Action; later pages load on demand, each scoped to the authenticated
305
+ * customer's channel. The backend's real `count` drives pagination, so a page
306
+ * length is never presented as the total. Selection is keyed by the REAL
307
+ * backend order id (via a number→id map), so an opened detail stays stable
308
+ * across paging and reload. No token is read, stored, or rendered here.
309
+ */
310
+ AccountOrders: ({ props }) => {
311
+ const { state, set } = useStateStore()
312
+ const parsed = accountOrdersPropsSchema.parse(props)
313
+ const authenticated = Boolean(state.accountAuthenticated)
314
+ const loadedForAuth = useRef(false)
315
+
316
+ const loadPage = (page: number) => {
317
+ set('/accountOrdersPending', true)
318
+ set('/accountOrdersError', null)
319
+ void loadOrdersPageAction({ page, pageSize: ORDERS_PAGE_SIZE })
320
+ .then((result) => {
321
+ set('/accountOrdersPending', false)
322
+ if (result.ok) {
323
+ set('/accountOrders', result.orders)
324
+ set('/accountOrdersCount', result.count)
325
+ set('/accountOrdersPage', page)
326
+ set('/accountOrdersPageSize', result.limit)
327
+ set('/accountOrdersError', null)
328
+ return
329
+ }
330
+ set('/accountOrdersError', result.error)
331
+ })
332
+ .catch((error) => {
333
+ console.error(
334
+ '[storefront/account] loadOrders failed',
335
+ error instanceof Error ? error.message : error,
336
+ )
337
+ set('/accountOrdersPending', false)
338
+ set('/accountOrdersError', parsed.errorLabel ?? 'No se pudieron cargar tus pedidos.')
339
+ })
340
+ }
341
+
342
+ useEffect(() => {
343
+ // Load the first page when the customer becomes authenticated, and reload
344
+ // after a re-login, so the history appears without a manual reload. A
345
+ // guest load is never attempted; the flag resets when the session ends.
346
+ if (!authenticated) {
347
+ loadedForAuth.current = false
348
+ return
349
+ }
350
+ if (loadedForAuth.current) return
351
+ loadedForAuth.current = true
352
+ loadPage(1)
353
+ // eslint-disable-next-line react-hooks/exhaustive-deps
354
+ }, [authenticated])
355
+
356
+ const pending = Boolean(state.accountOrdersPending)
357
+ const error = typeof state.accountOrdersError === 'string' ? state.accountOrdersError : null
358
+ const orders = Array.isArray(state.accountOrders)
359
+ ? (state.accountOrders as AccountOrderView[])
360
+ : []
361
+ const count =
362
+ typeof state.accountOrdersCount === 'number' ? state.accountOrdersCount : orders.length
363
+ const page = typeof state.accountOrdersPage === 'number' ? state.accountOrdersPage : 1
364
+ const pageSize =
365
+ typeof state.accountOrdersPageSize === 'number' && state.accountOrdersPageSize > 0
366
+ ? state.accountOrdersPageSize
367
+ : ORDERS_PAGE_SIZE
368
+ const selectedId =
369
+ typeof state.accountSelectedOrderId === 'string' ? state.accountSelectedOrderId : null
370
+ // Selection is keyed by the REAL backend order id, never the display number.
371
+ const selected = selectedId ? orders.find((order) => order.id === selectedId) ?? null : null
372
+ // The block row carries the display number; resolve it back to the backend id.
373
+ const backendIdByBlockId = new Map(orders.map((order) => [order.number, order.id]))
374
+
375
+ if (pending) {
376
+ return (
377
+ <p className="text-muted-foreground mx-auto max-w-7xl px-4 py-8 text-sm">
378
+ {parsed.loadingLabel ?? 'Cargando pedidos…'}
379
+ </p>
380
+ )
381
+ }
382
+ if (error) {
383
+ return (
384
+ <p role="alert" className="text-destructive mx-auto max-w-7xl px-4 py-8 text-sm">
385
+ {error}
386
+ </p>
387
+ )
388
+ }
389
+ if (orders.length === 0) {
390
+ return (
391
+ <p className="text-muted-foreground mx-auto max-w-7xl px-4 py-8 text-sm">
392
+ {parsed.emptyLabel ?? 'Aún no tienes pedidos.'}
393
+ </p>
394
+ )
395
+ }
396
+ const totalPages = Math.max(1, Math.ceil(count / pageSize))
397
+ return (
398
+ <>
399
+ <OrderHistory2
400
+ orders={orders.map(toBlockOrder)}
401
+ titleLabel={parsed.titleLabel}
402
+ descriptionLabel={parsed.descriptionLabel}
403
+ // The block renders "Total de pedidos: {n}" from the PAGE length; pass a
404
+ // label without `{n}` so a page length is never shown as the total.
405
+ totalOrdersLabel={parsed.totalOrdersLabel ?? 'Pedidos'}
406
+ onViewDetails={(order) =>
407
+ set('/accountSelectedOrderId', backendIdByBlockId.get(order.id) ?? null)
408
+ }
409
+ />
410
+ {count > pageSize ? (
411
+ <div className="mx-auto flex w-full max-w-7xl items-center justify-between gap-4 px-4 pb-6 sm:px-6 lg:px-8">
412
+ <p className="text-muted-foreground text-sm" role="status" aria-live="polite">
413
+ {`${count} pedidos · página ${page} de ${totalPages}`}
414
+ </p>
415
+ <div className="flex gap-2">
416
+ <Button
417
+ variant="outline"
418
+ size="sm"
419
+ disabled={pending || page <= 1}
420
+ onClick={() => loadPage(page - 1)}
421
+ >
422
+ Anterior
423
+ </Button>
424
+ <Button
425
+ variant="outline"
426
+ size="sm"
427
+ disabled={pending || page >= totalPages}
428
+ onClick={() => loadPage(page + 1)}
429
+ >
430
+ Siguiente
431
+ </Button>
432
+ </div>
433
+ </div>
434
+ ) : null}
435
+ {selected ? <OrderDetail order={selected} /> : null}
436
+ </>
437
+ )
438
+ },
439
+ }
@@ -0,0 +1,53 @@
1
+ // Thor Commerce view schema fragment: account
2
+ // Zod mirrors of the block contracts for this view — the single source for the
3
+ // catalog entries and the client-side registry parsing (json-render does not
4
+ // validate props against the catalog itself, so the registry parses here).
5
+ // Kept free of React and server imports so the Server Component module and the
6
+ // 'use client' registry can share one contract. Block zod contracts are
7
+ // mirrored, not imported: @thorprovider/blocks ships zod v3 while the scaffold
8
+ // runs zod v4 and cross-major schema assignment breaks the catalog contract.
9
+
10
+ import { z } from 'zod'
11
+
12
+ // Mirrors the LoginPage1 optional props. Every prop is optional with a neutral
13
+ // default in the block, so the curated view forwards only the labels it owns.
14
+ export const accountLoginPropsSchema = z.object({
15
+ logoText: z.string().optional(),
16
+ welcomeTitleLabel: z.string().optional(),
17
+ welcomeDescriptionLabel: z.string().optional(),
18
+ emailLabel: z.string().optional(),
19
+ passwordLabel: z.string().optional(),
20
+ signInLabel: z.string().optional(),
21
+ createAccountLabel: z.string().optional(),
22
+ })
23
+
24
+ // Mirrors the SignUpPage1 optional props.
25
+ export const accountRegisterPropsSchema = z.object({
26
+ logoText: z.string().optional(),
27
+ createAccountTitleLabel: z.string().optional(),
28
+ createAccountDescriptionLabel: z.string().optional(),
29
+ fullNameLabel: z.string().optional(),
30
+ emailLabel: z.string().optional(),
31
+ passwordLabel: z.string().optional(),
32
+ createAccountLabel: z.string().optional(),
33
+ signInLabel: z.string().optional(),
34
+ })
35
+
36
+ // Mirrors the OrderHistory2 label props the curated view forwards, plus the
37
+ // explicit loading/empty/error copy the registry renders for the orders states.
38
+ export const accountOrdersPropsSchema = z.object({
39
+ titleLabel: z.string().optional(),
40
+ descriptionLabel: z.string().optional(),
41
+ totalOrdersLabel: z.string().optional(),
42
+ emptyLabel: z.string().optional(),
43
+ errorLabel: z.string().optional(),
44
+ loadingLabel: z.string().optional(),
45
+ })
46
+
47
+ // The headless bootstrap and the signed-in panel read their data from the
48
+ // feature state, not from element props; declared so the catalog is complete.
49
+ export const accountEmptyPropsSchema = z.object({})
50
+
51
+ export type AccountLoginProps = z.infer<typeof accountLoginPropsSchema>
52
+ export type AccountRegisterProps = z.infer<typeof accountRegisterPropsSchema>
53
+ export type AccountOrdersProps = z.infer<typeof accountOrdersPropsSchema>
@@ -0,0 +1,11 @@
1
+ // Thor Commerce view content: cart (user-owned)
2
+ // Merchant-editable copy for the standalone cart review page. Seeded once by the
3
+ // scaffolder and never overwritten on sync.
4
+
5
+ export const cartContent = {
6
+ titleLabel: 'Tu carrito',
7
+ checkoutLabel: 'Finalizar compra',
8
+ continueShoppingLabel: 'Seguir comprando',
9
+ emptyLabel: 'Tu carrito está vacío',
10
+ readErrorLabel: 'No se pudo cargar tu carrito.',
11
+ }
@@ -0,0 +1,23 @@
1
+ // Thor Commerce view data: cart (user-owned)
2
+ // Pure mapping from the shared host cart state to the ShoppingCart4 block props.
3
+ // No React and no server imports so it can be imported from either boundary.
4
+
5
+ import type { ShoppingCart4Item } from '@thorprovider/blocks'
6
+ import type { CartLineState } from '../../state'
7
+
8
+ /**
9
+ * Maps backend cart lines to the cart block's item shape. `price` stays 0 (the
10
+ * legacy numeric field) because the block renders the canonical `lineTotal`
11
+ * Money; an absent image is left undefined rather than serialized as "".
12
+ */
13
+ export const toCartViewItems = (cart: CartLineState[]): ShoppingCart4Item[] =>
14
+ cart.map(line => ({
15
+ id: line.id,
16
+ name: line.name,
17
+ price: 0,
18
+ quantity: line.quantity,
19
+ image: line.image,
20
+ variant: line.variant,
21
+ lineTotal: line.lineTotal,
22
+ unitPrice: line.unitPrice,
23
+ }))
@@ -0,0 +1,53 @@
1
+ // Thor Commerce view: cart
2
+ // The standalone cart review route. Its data comes from the shared host cart
3
+ // state injected on every route (readCartStateForRender), so the loader only
4
+ // reports the data source — no dedicated cart fetch happens here.
5
+
6
+ import { getDataSource } from '../../store-context'
7
+ import { pageRoot } from '../../spec/page-root'
8
+ import type { StorefrontElement } from '../../spec/types'
9
+ import type { ViewModule } from '../types'
10
+ import { cartViewProps } from './props'
11
+ import { registry } from './registry'
12
+ import { cartViewPropsSchema } from './schema'
13
+
14
+ const elements: Record<string, StorefrontElement> = {
15
+ 'cart.view': {
16
+ type: 'CartView',
17
+ props: cartViewProps,
18
+ on: {
19
+ refreshCart: { action: 'refreshCart' },
20
+ updateQuantity: { action: 'updateQuantity' },
21
+ removeFromCart: { action: 'removeFromCart' },
22
+ goToCheckout: { action: 'goToCheckout' },
23
+ goHome: { action: 'goHome' },
24
+ },
25
+ },
26
+ }
27
+
28
+ const catalog = {
29
+ CartView: {
30
+ props: cartViewPropsSchema,
31
+ description: 'Standalone cart review page with backend-authoritative totals.',
32
+ },
33
+ }
34
+
35
+ export const cart: ViewModule = {
36
+ name: 'cart',
37
+ elements,
38
+ catalog,
39
+ registry,
40
+ route: {
41
+ path: '/cart',
42
+ loaderName: 'cart',
43
+ layout: 'default',
44
+ metadata: {
45
+ title: 'Carrito',
46
+ description: 'Revisa los artículos de tu carrito antes de finalizar la compra.',
47
+ },
48
+ entry: pageRoot('cart', ['cart.view'], elements),
49
+ },
50
+ loader: {
51
+ cart: async () => ({ dataSource: getDataSource(), dataError: null }),
52
+ },
53
+ }
@@ -0,0 +1,12 @@
1
+ // Thor Commerce view props: cart (user-owned)
2
+ // Element props for the cart review view. The cart lines and backend-authoritative
3
+ // totals are read from the shared host state (injected for every route), so this
4
+ // view needs no dedicated data fetch.
5
+
6
+ import { cartContent } from './content'
7
+
8
+ export const cartViewProps = {
9
+ ...cartContent,
10
+ items: { $state: '/cart' },
11
+ totals: { $state: '/cartTotals' },
12
+ }
@@ -0,0 +1,81 @@
1
+ 'use client'
2
+
3
+ // Thor Commerce view registry: cart
4
+ // Renders the standalone cart review page (page-mode ShoppingCart4) from the
5
+ // shared host cart state. Quantity updates use the ABSOLUTE line quantity keyed
6
+ // by backend line id; removal uses the same line id. Checkout is only offered
7
+ // when the checkout capability is installed and the cart is ready.
8
+
9
+ import { Button, ShoppingCart4 } from '@thorprovider/blocks'
10
+ import { isAvailableMoney } from '@thorprovider/blocks/contracts'
11
+ import { useStateStore } from '@json-render/react'
12
+ import { checkoutAvailable } from '../../composition'
13
+ import type { ViewRegistry } from '../types'
14
+ import { toCartViewItems } from './data'
15
+ import { cartViewPropsSchema } from './schema'
16
+
17
+ export const registry: ViewRegistry = {
18
+ CartView: ({ props, emit }) => {
19
+ const { state, set } = useStateStore()
20
+ const parsed = cartViewPropsSchema.parse(props)
21
+ const cart = parsed.items
22
+ const cartError = typeof state.cartError === 'string' ? state.cartError : undefined
23
+ const readError = typeof state.cartReadError === 'string' ? state.cartReadError : undefined
24
+ const isPending = state.cartPending === true
25
+ const errorLabel = cartError ?? readError
26
+ const totalsState = isPending ? 'pending' : errorLabel ? 'error' : 'idle'
27
+ // Checkout is offered only when the required backend amounts are present and
28
+ // valid: a missing/malformed subtotal, total, or line amount blocks it, while
29
+ // an undetermined shipping amount does not (AC-CART-4.4 / UC-CART-7).
30
+ const amountsValid =
31
+ cart.every((line) => isAvailableMoney(line.lineTotal)) &&
32
+ parsed.totals != null &&
33
+ isAvailableMoney(parsed.totals.subtotal) &&
34
+ isAvailableMoney(parsed.totals.total)
35
+
36
+ // A read failure with no confirmed lines is not an empty cart: surface it as
37
+ // a recoverable error rather than the empty state.
38
+ if (readError && cart.length === 0) {
39
+ return (
40
+ <section className='mx-auto max-w-3xl px-4 py-8'>
41
+ <div role='alert' className='flex flex-col items-start gap-3'>
42
+ <p className='text-destructive text-sm'>{parsed.readErrorLabel ?? readError}</p>
43
+ <Button variant='outline' size='sm' onClick={() => emit('refreshCart')}>
44
+ Reintentar
45
+ </Button>
46
+ </div>
47
+ </section>
48
+ )
49
+ }
50
+
51
+ return (
52
+ <ShoppingCart4
53
+ presentation='page'
54
+ items={toCartViewItems(cart)}
55
+ promotions={[]}
56
+ totals={parsed.totals ?? undefined}
57
+ totalsState={totalsState}
58
+ totalsErrorLabel={errorLabel}
59
+ titleLabel={parsed.titleLabel}
60
+ checkoutLabel={parsed.checkoutLabel}
61
+ continueShoppingLabel={parsed.continueShoppingLabel}
62
+ emptyLabel={parsed.emptyLabel}
63
+ onRetryTotals={() => emit('refreshCart')}
64
+ onQuantityChange={(id, quantity) => {
65
+ const line = cart.find(item => item.id === id)
66
+ if (!line) return
67
+ set('/qtyUpdate', { id, merchandiseId: line.merchandiseId, quantity })
68
+ emit('updateQuantity')
69
+ }}
70
+ onRemove={id => {
71
+ set('/lastRemoved', id)
72
+ emit('removeFromCart')
73
+ }}
74
+ onCheckout={() => emit('goToCheckout')}
75
+ onContinueShopping={() => emit('goHome')}
76
+ checkoutUnavailable={!checkoutAvailable}
77
+ checkoutDisabled={isPending || Boolean(errorLabel) || cart.length === 0 || !amountsValid}
78
+ />
79
+ )
80
+ },
81
+ }