@thorprovider/blocks 0.1.3 → 1.0.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 (99) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +73 -29
  3. package/docs/standards/data-contracts.md +13 -2
  4. package/package.json +26 -10
  5. package/scripts/fix-esm-extensions.mjs +40 -0
  6. package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
  7. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  8. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  9. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  10. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  11. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  12. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  13. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  14. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  15. package/src/blocks/e-commerce/product-list/product-card.tsx +424 -0
  16. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  17. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  18. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  19. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  20. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  21. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  22. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  23. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  24. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  25. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  26. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
  27. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  28. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  29. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  30. package/src/blocks/marketing/features/feature.tsx +195 -0
  31. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +27 -9
  32. package/src/blocks/marketing/navbars/navbar-4.test.tsx +33 -2
  33. package/src/blocks/marketing/navbars/navbar-4.tsx +29 -11
  34. package/src/index.ts +77 -48
  35. package/src/lib/contracts-curated.test.ts +42 -24
  36. package/src/lib/contracts.test.ts +143 -2
  37. package/src/lib/contracts.ts +346 -14
  38. package/src/ui/button-group.tsx +1 -1
  39. package/src/ui/carousel.tsx +1 -1
  40. package/src/ui/dialog.tsx +1 -1
  41. package/src/ui/field.tsx +2 -2
  42. package/src/ui/input-group.tsx +3 -3
  43. package/src/ui/pagination.tsx +1 -1
  44. package/src/ui/sidebar.tsx +7 -7
  45. package/src/ui/toggle-group.tsx +1 -1
  46. package/tsconfig.build.json +12 -0
  47. package/tsup.config.ts +11 -7
  48. package/.turbo/turbo-build.log +0 -24
  49. package/.turbo/turbo-test.log +0 -47
  50. package/.turbo/turbo-type-check.log +0 -0
  51. package/dist/.tsbuildinfo +0 -1
  52. package/dist/contracts.d.mts +0 -1732
  53. package/dist/contracts.d.ts +0 -1732
  54. package/dist/contracts.js +0 -423
  55. package/dist/contracts.js.map +0 -1
  56. package/dist/contracts.mjs +0 -365
  57. package/dist/contracts.mjs.map +0 -1
  58. package/dist/index.d.mts +0 -3068
  59. package/dist/index.d.ts +0 -3068
  60. package/dist/index.js +0 -17834
  61. package/dist/index.js.map +0 -1
  62. package/dist/index.mjs +0 -17740
  63. package/dist/index.mjs.map +0 -1
  64. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  65. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  66. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  67. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  68. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  69. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  70. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
  71. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
  72. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  73. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  74. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  75. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  76. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  77. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  78. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  79. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  80. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  81. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  82. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  83. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  84. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  85. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  86. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  87. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  88. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  89. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  90. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  91. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  92. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
  93. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
  94. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  95. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  96. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  97. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  98. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  99. package/src/blocks/marketing/features/feature-2.tsx +0 -82
package/src/index.ts CHANGED
@@ -23,16 +23,18 @@ export { default as Navbar4 } from './blocks/marketing/navbars/navbar-4'
23
23
  export { default as Banner1 } from './blocks/marketing/banners/banner-1'
24
24
  export { default as HeroSection1 } from './blocks/marketing/hero-sections/hero-section-1'
25
25
  export { default as CtaSection1 } from './blocks/marketing/cta/cta-section-1'
26
- export { default as Feature1 } from './blocks/marketing/features/feature-1'
27
- export { default as Feature2 } from './blocks/marketing/features/feature-2'
26
+ export { default as Feature } from './blocks/marketing/features/feature'
28
27
  export { default as Testimonial1 } from './blocks/marketing/testimonials/testimonial-1'
29
28
  export { default as Testimonial2 } from './blocks/marketing/testimonials/testimonial-2'
30
29
  export { default as FaqBlock1 } from './blocks/marketing/faq/faq-block-1'
31
30
  export { default as PricingSection1 } from './blocks/marketing/pricing/pricing-section-1'
32
31
  export type { CtaSection1Props } from './blocks/marketing/cta/cta-section-1'
33
32
  export type { FaqBlock1Props, FaqItem } from './blocks/marketing/faq/faq-block-1'
34
- export type { Feature1Props, Feature1Item } from './blocks/marketing/features/feature-1'
35
- export type { Feature2Props, Feature2Item } from './blocks/marketing/features/feature-2'
33
+ export type {
34
+ FeatureProps,
35
+ FeatureItem,
36
+ FeaturePresentation,
37
+ } from './blocks/marketing/features/feature'
36
38
  export type { LogoCloud1Props, LogoCloud1Platform } from './blocks/marketing/logo-clouds/logo-cloud-1'
37
39
  export type { PricingSection1Props, PricingTier } from './blocks/marketing/pricing/pricing-section-1'
38
40
  export type { Statistic1Props, StatisticItem } from './blocks/marketing/statistics/statistic-1'
@@ -52,41 +54,43 @@ export type {
52
54
  FooterSection4SocialIcon,
53
55
  FooterSection4SocialLink,
54
56
  } from './blocks/marketing/footers/footer-section-4'
55
- export { default as ProductCard1 } from './blocks/e-commerce/product-list/product-card-1'
56
- export { default as ProductCard2 } from './blocks/e-commerce/product-list/product-card-2'
57
- export { default as ProductCard3 } from './blocks/e-commerce/product-list/product-card-3'
58
- export { default as ProductCard5 } from './blocks/e-commerce/product-list/product-card-5'
59
- export { default as ProductShowcase1 } from './blocks/e-commerce/product-list/product-showcase-1'
57
+ export { ProductCard } from './blocks/e-commerce/product-list/product-card'
60
58
  export type {
61
- ProductCard1Product,
62
- ProductCard1Props,
63
- } from './blocks/e-commerce/product-list/product-card-1'
59
+ ProductCardProps,
60
+ ProductCardPresentation,
61
+ } from './blocks/e-commerce/product-list/product-card'
62
+ export { ProductGrid } from './blocks/e-commerce/product-list/product-grid'
63
+ export type { ProductGridProps } from './blocks/e-commerce/product-list/product-grid'
64
+ export { default as ProductShowcase1 } from './blocks/e-commerce/product-list/product-showcase-1'
64
65
  export type { ProductShowcase1Props } from './blocks/e-commerce/product-list/product-showcase-1'
65
- export { default as ProductOverview1 } from './blocks/e-commerce/product-overview/product-overview-1'
66
+ export { default as ProductOverview } from './blocks/e-commerce/product-overview/product-overview'
66
67
  export type {
67
- ProductOverview1Props,
68
- ProductOverview1Rating,
68
+ ProductOverviewProps,
69
+ ProductOverviewRating,
70
+ ProductOverviewPresentation,
71
+ ProductOverviewReview,
69
72
  ProductDetailProduct,
70
73
  ProductDetailVariant,
71
- } from './blocks/e-commerce/product-overview/product-overview-1'
72
- export { default as ProductOverview2 } from './blocks/e-commerce/product-overview/product-overview-2'
73
- export { default as ProductOverview5 } from './blocks/e-commerce/product-overview/product-overview-5'
74
- export type { ProductOverview2Props, ProductOverview2Product } from './blocks/e-commerce/product-overview/product-overview-2'
75
- export type { ProductOverview5Props, ProductOverview5Product, ProductOverview5Review } from './blocks/e-commerce/product-overview/product-overview-5'
74
+ } from './blocks/e-commerce/product-overview/product-overview'
76
75
  export { default as ProductQuickView } from './blocks/e-commerce/product-overview/product-quick-view'
77
76
  export { default as ReviewRating1 } from './blocks/e-commerce/reviews/review-rating-1'
78
77
  export type { ReviewRating1Review } from './blocks/e-commerce/reviews/review-rating-1'
79
78
  export { default as ReviewRating2 } from './blocks/e-commerce/reviews/review-rating-2'
80
- export { default as ProductCategory1 } from './blocks/e-commerce/product-categories/product-category-1'
79
+ export { default as ProductCategory } from './blocks/e-commerce/product-categories/product-category'
81
80
  export type {
82
- ProductCategory1Category,
83
- ProductCategory1CategoryIcon,
84
- ProductCategory1Props,
85
- } from './blocks/e-commerce/product-categories/product-category-1'
86
- export { default as ProductCategory2 } from './blocks/e-commerce/product-categories/product-category-2'
87
- export type { ProductCategory2Category, ProductCategory2Props } from './blocks/e-commerce/product-categories/product-category-2'
88
- export { default as ProductCategory4 } from './blocks/e-commerce/product-categories/product-category-4'
89
- export { default as ProductCategory5 } from './blocks/e-commerce/product-categories/product-category-5'
81
+ ProductCategoryProps,
82
+ ProductCategoryPresentation,
83
+ } from './blocks/e-commerce/product-categories/product-category'
84
+ export { default as CategoryCard } from './blocks/e-commerce/product-categories/category-card'
85
+ export type {
86
+ CategoryCardProps,
87
+ CategoryCardPresentation,
88
+ } from './blocks/e-commerce/product-categories/category-card'
89
+ export { default as CategoryGrid } from './blocks/e-commerce/product-categories/product-category-grid'
90
+ export type {
91
+ CategoryGridProps,
92
+ CategoryGridPresentation,
93
+ } from './blocks/e-commerce/product-categories/product-category-grid'
90
94
  export { MegaMenu, megaMenuPropsSchema } from './blocks/e-commerce/navigation/mega-menu'
91
95
  export type { MegaMenuProps } from './blocks/e-commerce/navigation/mega-menu'
92
96
  export { MobileCategoryDrawer, mobileCategoryDrawerPropsSchema } from './blocks/e-commerce/navigation/mobile-category-drawer'
@@ -128,13 +132,13 @@ export type {
128
132
  } from './blocks/e-commerce/category-filters/category-filter-2'
129
133
  export { default as CategoryFilter4 } from './blocks/e-commerce/category-filters/category-filter-4'
130
134
  export type { CategoryFilter4Props, CategoryFilter4Product, CategoryFilter4Facets } from './blocks/e-commerce/category-filters/category-filter-4'
131
- export { default as ShoppingCart1 } from './blocks/e-commerce/shopping-carts/shopping-cart-1'
132
- export { default as ShoppingCart2 } from './blocks/e-commerce/shopping-carts/shopping-cart-2'
133
- export { default as ShoppingCart4 } from './blocks/e-commerce/shopping-carts/shopping-cart-4'
134
- export { default as ShoppingCart3 } from './blocks/e-commerce/shopping-carts/shopping-cart-3'
135
- export type { ShoppingCart3Props, ShoppingCart3CartItem } from './blocks/e-commerce/shopping-carts/shopping-cart-3'
136
- export type { ShoppingCart2Props, ShoppingCart2Item } from './blocks/e-commerce/shopping-carts/shopping-cart-2'
137
- export type { ShoppingCart4Props, ShoppingCart4Item } from './blocks/e-commerce/shopping-carts/shopping-cart-4'
135
+ export { default as ShoppingCart } from './blocks/e-commerce/shopping-carts/shopping-cart'
136
+ export type {
137
+ ShoppingCartProps,
138
+ ShoppingCartPromotion,
139
+ ShoppingCartPresentation,
140
+ ShoppingCartTotalsState,
141
+ } from './blocks/e-commerce/shopping-carts/shopping-cart'
138
142
  export { default as StoreContainer1 } from './blocks/e-commerce/store-containers/store-container-1'
139
143
  export type { StoreContainer1Props } from './blocks/e-commerce/store-containers/store-container-1'
140
144
  export { default as CheckoutForm1 } from './blocks/e-commerce/checkout-forms/checkout-form-1'
@@ -181,14 +185,18 @@ export type {
181
185
  export type { CheckoutFieldError, CheckoutFieldErrors } from './blocks/e-commerce/checkout-forms/checkout-form-types'
182
186
  export { default as OrderHistory1 } from './blocks/e-commerce/order-history/order-history-1'
183
187
  export { default as OrderHistory2 } from './blocks/e-commerce/order-history/order-history-2'
184
- export { default as StorefrontHero1 } from './blocks/e-commerce/storefront-hero/storefront-hero-1'
188
+ export { default as StorefrontHero } from './blocks/e-commerce/storefront-hero/storefront-hero'
185
189
  export type {
186
190
  FeaturedProduct,
187
- StorefrontHero1Props,
188
- } from './blocks/e-commerce/storefront-hero/storefront-hero-1'
189
- export { default as StorefrontHero2 } from './blocks/e-commerce/storefront-hero/storefront-hero-2'
190
- export { default as StorefrontHero5 } from './blocks/e-commerce/storefront-hero/storefront-hero-5'
191
- export { default as StorefrontHero6 } from './blocks/e-commerce/storefront-hero/storefront-hero-6'
191
+ HeroImage,
192
+ HeroDepartment,
193
+ HeroFeaturedDeal,
194
+ HeroStoreLocator,
195
+ HeroDeal,
196
+ HeroCategory,
197
+ StorefrontHeroPresentation,
198
+ StorefrontHeroProps,
199
+ } from './blocks/e-commerce/storefront-hero/storefront-hero'
192
200
  export { default as AppShell1 } from './blocks/application/app-shells/app-shell-1'
193
201
  export { default as LoginPage1 } from './blocks/application/login-forms/login-page-1'
194
202
  export { default as LoginPage2 } from './blocks/application/login-forms/login-page-2'
@@ -215,13 +223,18 @@ export {
215
223
  cartTotalsSchema,
216
224
  productDetailProductSchema,
217
225
  productDetailVariantSchema,
218
- productOverview1PropsSchema,
226
+ productOverviewPropsSchema,
227
+ storefrontHeroPropsSchema,
228
+ storefrontHeroProductSchema,
219
229
  buttonPropsSchema,
220
230
  storeContainer1PropsSchema,
221
- productCard1PropsSchema,
231
+ productCardPropsSchema,
232
+ productGridPropsSchema,
222
233
  productPresentationSchema,
223
234
  productShowcase1PropsSchema,
224
- shoppingCart4PropsSchema,
235
+ shoppingCartPropsSchema,
236
+ shoppingCartItemSchema,
237
+ shoppingCartSuggestionSchema,
225
238
  navbar4PropsSchema,
226
239
  paymentMethodSchema,
227
240
  cardProductSchema,
@@ -232,18 +245,27 @@ export {
232
245
  navCartItemSchema,
233
246
  storeUserSchema,
234
247
  accountMenuItemSchema,
248
+ featurePropsSchema,
249
+ featureCardsSpecSchema,
250
+ featureSplitSpecSchema,
251
+ productCategoryItemSchema,
252
+ productCategoryPropsSchema,
235
253
  } from './lib/contracts'
236
254
  export type {
237
255
  ProductDetailProductSpec,
238
256
  ProductDetailVariantSpec,
239
- ProductOverview1SpecProps,
257
+ StorefrontHeroProduct,
258
+ StorefrontHeroSpecProps,
240
259
  CartTotals,
241
260
  ButtonSpecProps,
242
261
  StoreContainer1SpecProps,
243
- ProductCard1SpecProps,
262
+ ProductCardSpecProps,
263
+ ProductGridSpecProps,
244
264
  ProductPresentation,
245
265
  ProductShowcase1SpecProps,
246
- ShoppingCart4SpecProps,
266
+ ShoppingCartItem,
267
+ ShoppingCartSuggestion,
268
+ ShoppingCartSpecProps,
247
269
  Navbar4SpecProps,
248
270
  CardProduct,
249
271
  CartItem,
@@ -253,4 +275,11 @@ export type {
253
275
  NavCartItem,
254
276
  StoreUser,
255
277
  AccountMenuItem,
278
+ FeatureSpecProps,
279
+ FeatureCardsSpecProps,
280
+ FeatureSplitSpecProps,
281
+ ProductCategoryItem,
282
+ ProductCategorySpecProps,
283
+ CategoryCardSpecProps,
284
+ CategoryGridSpecProps,
256
285
  } from './lib/contracts'
@@ -11,8 +11,8 @@ import {
11
11
  moneySchema,
12
12
  navCartItemSchema,
13
13
  navbar4PropsSchema,
14
- productOverview1PropsSchema,
15
- shoppingCart4PropsSchema,
14
+ productOverviewPropsSchema,
15
+ shoppingCartPropsSchema,
16
16
  } from './contracts'
17
17
 
18
18
  const product = {
@@ -55,13 +55,13 @@ describe('moneySchema', () => {
55
55
  })
56
56
  })
57
57
 
58
- describe('productOverview1PropsSchema', () => {
58
+ describe('productOverviewPropsSchema', () => {
59
59
  it('accepts a product whose variant price is omitted or null (unavailable)', () => {
60
- expect(productOverview1PropsSchema.safeParse({ product }).success).toBe(true)
60
+ expect(productOverviewPropsSchema.safeParse({ product }).success).toBe(true)
61
61
  })
62
62
 
63
63
  it('accepts an explicit controlled selected variant id', () => {
64
- expect(productOverview1PropsSchema.safeParse({ product, selectedVariantId: 'v1' }).success).toBe(true)
64
+ expect(productOverviewPropsSchema.safeParse({ product, selectedVariantId: 'v1' }).success).toBe(true)
65
65
  })
66
66
 
67
67
  it('rejects a variant missing selectedOptions', () => {
@@ -69,7 +69,7 @@ describe('productOverview1PropsSchema', () => {
69
69
  ...product,
70
70
  variants: [{ id: 'v1', title: 'x', availableForSale: true, price: null }],
71
71
  }
72
- expect(productOverview1PropsSchema.safeParse({ product: bad }).success).toBe(false)
72
+ expect(productOverviewPropsSchema.safeParse({ product: bad }).success).toBe(false)
73
73
  })
74
74
  })
75
75
 
@@ -147,24 +147,6 @@ describe('optional brand seams — present, omitted, invalid', () => {
147
147
  })
148
148
  })
149
149
 
150
- describe('shoppingCart4PropsSchema — authoritative totals', () => {
151
- it('accepts backend totals and a controlled totals state', () => {
152
- const result = shoppingCart4PropsSchema.safeParse({
153
- items: [],
154
- totals: {
155
- subtotal: { amount: '0', currencyCode: 'USD' },
156
- total: { amount: '0', currencyCode: 'USD' },
157
- },
158
- totalsState: 'idle',
159
- })
160
- expect(result.success).toBe(true)
161
- })
162
-
163
- it('rejects an unknown totals state', () => {
164
- expect(shoppingCart4PropsSchema.safeParse({ items: [], totalsState: 'loading' }).success).toBe(false)
165
- })
166
- })
167
-
168
150
  describe('faqBlock1PropsSchema / ctaSection1PropsSchema — bounded editorial sections', () => {
169
151
  const faqItem = { value: 'q1', question: '¿Envían a todo el país?', answer: 'Sí.' }
170
152
 
@@ -205,3 +187,39 @@ describe('faqBlock1PropsSchema / ctaSection1PropsSchema — bounded editorial se
205
187
  expect('onPrimaryClick' in parsed).toBe(false)
206
188
  })
207
189
  })
190
+
191
+ describe('shoppingCartPropsSchema — canonical Money-only cart lines', () => {
192
+ const money = { amount: '22.22', currencyCode: 'USD' }
193
+
194
+ it('accepts a canonical Money line keyed by backend line id', () => {
195
+ const result = shoppingCartPropsSchema.safeParse({
196
+ items: [{ id: 'line_1', name: 'Teclado', quantity: 2, unitPrice: money, lineTotal: money }],
197
+ totals: { subtotal: money, total: money },
198
+ totalsState: 'idle',
199
+ })
200
+ expect(result.success).toBe(true)
201
+ })
202
+
203
+ it('rejects a legacy numeric-price line without canonical Money', () => {
204
+ const result = shoppingCartPropsSchema.safeParse({
205
+ items: [{ id: 'line_1', name: 'Teclado', price: 10, quantity: 2 }],
206
+ })
207
+ expect(result.success).toBe(false)
208
+ })
209
+
210
+ it('rejects an unknown totals state', () => {
211
+ const result = shoppingCartPropsSchema.safeParse({
212
+ items: [],
213
+ totalsState: 'loading',
214
+ })
215
+ expect(result.success).toBe(false)
216
+ })
217
+
218
+ it('omits an absent suggestion image and price honestly', () => {
219
+ const result = shoppingCartPropsSchema.safeParse({
220
+ items: [{ id: 'line_1', name: 'x', quantity: 1, unitPrice: money, lineTotal: money }],
221
+ recentlyViewed: [{ id: 'p1', name: 'Mouse' }],
222
+ })
223
+ expect(result.success).toBe(true)
224
+ })
225
+ })
@@ -1,5 +1,12 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { storeCategorySchema } from './contracts'
2
+ import { z } from 'zod'
3
+ import {
4
+ categoryCardPropsSchema,
5
+ categoryGridPropsSchema,
6
+ productCardPropsSchema,
7
+ productGridPropsSchema,
8
+ storeCategorySchema,
9
+ } from './contracts'
3
10
 
4
11
  describe('storeCategorySchema', () => {
5
12
  it('accepts flat categories (additive backward compatibility)', () => {
@@ -29,4 +36,138 @@ describe('storeCategorySchema', () => {
29
36
  expect(storeCategorySchema.safeParse({ label: 'Solo', href: '/categorias/x', children: [{}] }).success).toBe(false)
30
37
  expect(storeCategorySchema.safeParse({}).success).toBe(false)
31
38
  })
32
- })
39
+ })
40
+
41
+ describe('productCardPropsSchema', () => {
42
+ const canonicalItem = {
43
+ id: 'sku-1',
44
+ title: 'Llave de impacto',
45
+ price: { amount: '129.50', currencyCode: 'EUR' },
46
+ }
47
+
48
+ it('accepts a single canonical Money item with the bounded presentation options', () => {
49
+ expect(productCardPropsSchema.safeParse({ item: canonicalItem }).success).toBe(true)
50
+ expect(
51
+ productCardPropsSchema.safeParse({
52
+ item: canonicalItem,
53
+ presentation: 'classic',
54
+ wishlisted: true,
55
+ }).success,
56
+ ).toBe(true)
57
+ expect(
58
+ productCardPropsSchema.safeParse({ item: canonicalItem, presentation: 'rich' }).success,
59
+ ).toBe(true)
60
+ })
61
+
62
+ it('rejects a collection in place of the single item', () => {
63
+ expect(productCardPropsSchema.safeParse({ items: [canonicalItem] }).success).toBe(false)
64
+ })
65
+
66
+ it('rejects legacy numeric-price items', () => {
67
+ expect(
68
+ productCardPropsSchema.safeParse({ item: { id: 'p1', title: 'Taladro', price: 49.99, image: '/x.svg' } })
69
+ .success,
70
+ ).toBe(false)
71
+ })
72
+
73
+ it('rejects unknown presentation options', () => {
74
+ expect(
75
+ productCardPropsSchema.safeParse({ item: canonicalItem, presentation: 'compact' }).success,
76
+ ).toBe(false)
77
+ })
78
+ })
79
+
80
+ describe('productGridPropsSchema', () => {
81
+ const canonicalItem = {
82
+ id: 'sku-1',
83
+ title: 'Llave de impacto',
84
+ price: { amount: '129.50', currencyCode: 'EUR' },
85
+ }
86
+
87
+ it('accepts a collection with the listing state options', () => {
88
+ expect(productGridPropsSchema.safeParse({ items: [canonicalItem] }).success).toBe(true)
89
+ expect(
90
+ productGridPropsSchema.safeParse({
91
+ items: [canonicalItem],
92
+ presentation: 'minimal',
93
+ heading: 'Destacados',
94
+ maxItems: 6,
95
+ wishlistedIds: ['sku-1'],
96
+ loading: false,
97
+ skeletonCount: 6,
98
+ emptyLabel: 'Sin productos',
99
+ }).success,
100
+ ).toBe(true)
101
+ })
102
+
103
+ it('declares only serializable keys — no callbacks or label strings', () => {
104
+ const runtimeOnlyKeys = [
105
+ 'onAddToCart',
106
+ 'onProductClick',
107
+ 'onToggleWishlist',
108
+ 'onViewSimilar',
109
+ 'addToCartLabel',
110
+ 'unavailablePriceLabel',
111
+ 'addToWishlistLabel',
112
+ 'removeFromWishlistLabel',
113
+ 'viewSimilarLabel',
114
+ ]
115
+ const schemaOwnedLabelKeys = new Set(['emptyLabel'])
116
+ const schemas: Array<{ name: string; schema: z.AnyZodObject }> = [
117
+ { name: 'productCardPropsSchema', schema: productCardPropsSchema },
118
+ { name: 'productGridPropsSchema', schema: productGridPropsSchema },
119
+ ]
120
+
121
+ for (const { name, schema } of schemas) {
122
+ const declaredKeys = Object.keys(schema.shape)
123
+ expect(declaredKeys.length, name).toBeGreaterThan(0)
124
+ for (const key of declaredKeys) {
125
+ expect(runtimeOnlyKeys, `${name}.${key}`).not.toContain(key)
126
+ expect(key, `${name}.${key}`).not.toMatch(/^on[A-Z]/)
127
+ if (!schemaOwnedLabelKeys.has(key)) {
128
+ expect(key, `${name}.${key}`).not.toMatch(/Label$/)
129
+ }
130
+ expect(schema.shape[key], `${name}.${key}`).not.toBeInstanceOf(z.ZodFunction)
131
+ }
132
+ }
133
+ })
134
+
135
+ it('rejects non-positive maxItems and skeletonCount', () => {
136
+ expect(productGridPropsSchema.safeParse({ items: [], maxItems: 0 }).success).toBe(false)
137
+ expect(productGridPropsSchema.safeParse({ items: [], skeletonCount: 0 }).success).toBe(false)
138
+ })
139
+ })
140
+
141
+ describe('category cell and listing contracts', () => {
142
+ const category = { id: 'cat-tools', name: 'Herramientas' }
143
+
144
+ it('keeps category cell and listing props JSON-serializable and callback-free', () => {
145
+ const samples: Array<{ name: string; schema: z.AnyZodObject; props: unknown }> = [
146
+ {
147
+ name: 'categoryCardPropsSchema',
148
+ schema: categoryCardPropsSchema,
149
+ props: { category, presentation: 'grid', browseLabel: 'Explorar' },
150
+ },
151
+ {
152
+ name: 'categoryGridPropsSchema',
153
+ schema: categoryGridPropsSchema,
154
+ props: { categories: [category], presentation: 'grid', title: 'Categorías', maxItems: 4 },
155
+ },
156
+ ]
157
+
158
+ for (const { name, schema, props } of samples) {
159
+ const parsed = schema.parse(props)
160
+ expect(JSON.parse(JSON.stringify(parsed)), name).toEqual(parsed)
161
+ for (const key of Object.keys(schema.shape)) {
162
+ expect(key, `${name}.${key}`).not.toMatch(/^on[A-Z]/)
163
+ expect(schema.shape[key], `${name}.${key}`).not.toBeInstanceOf(z.ZodFunction)
164
+ }
165
+ }
166
+ })
167
+
168
+ it('keeps display labels serializable strings and rejects a collection in the cell contract', () => {
169
+ expect(categoryCardPropsSchema.safeParse({ category, browseLabel: () => 'Explorar' }).success).toBe(false)
170
+ expect(categoryGridPropsSchema.safeParse({ categories: [], emptyLabel: () => 'Vacío' }).success).toBe(false)
171
+ expect(categoryCardPropsSchema.safeParse({ categories: [category] }).success).toBe(false)
172
+ })
173
+ })