@thorprovider/blocks 0.1.3 → 2.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 (178) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/README.md +228 -68
  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/application/login-forms/login-page.test.tsx +190 -0
  7. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  8. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  9. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  10. package/src/blocks/application/verification/verify-1.tsx +2 -2
  11. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  12. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  13. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  14. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  15. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  16. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  17. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  19. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  20. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  21. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  22. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  23. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  24. package/src/blocks/e-commerce/navigation/storefront-navbar-account-menu.test.tsx +56 -0
  25. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  27. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  29. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  30. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +67 -36
  31. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +43 -25
  32. package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
  33. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  34. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  35. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  36. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  37. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  38. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  39. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  40. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  41. package/src/blocks/e-commerce/product-list/product-card.tsx +447 -0
  42. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  43. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  44. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  45. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  46. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  47. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  48. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  49. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  50. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  51. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  52. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
  53. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  54. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  55. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  56. package/src/blocks/marketing/features/feature.tsx +195 -0
  57. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  58. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  59. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  60. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  61. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  62. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  63. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  64. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  65. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  66. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  67. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  68. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  69. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  70. package/src/index.ts +156 -128
  71. package/src/lib/contracts-curated.test.ts +47 -29
  72. package/src/lib/contracts.test.ts +198 -2
  73. package/src/lib/contracts.ts +410 -16
  74. package/src/ui/button-group.tsx +1 -1
  75. package/src/ui/carousel.tsx +1 -1
  76. package/src/ui/dialog.tsx +1 -1
  77. package/src/ui/field.tsx +2 -2
  78. package/src/ui/input-group.tsx +3 -3
  79. package/src/ui/pagination.tsx +1 -1
  80. package/src/ui/sidebar.tsx +7 -7
  81. package/src/ui/toggle-group.tsx +1 -1
  82. package/tsconfig.build.json +12 -0
  83. package/tsup.config.ts +11 -7
  84. package/.turbo/turbo-build.log +0 -24
  85. package/.turbo/turbo-test.log +0 -47
  86. package/.turbo/turbo-type-check.log +0 -0
  87. package/dist/.tsbuildinfo +0 -1
  88. package/dist/contracts.d.mts +0 -1732
  89. package/dist/contracts.d.ts +0 -1732
  90. package/dist/contracts.js +0 -423
  91. package/dist/contracts.js.map +0 -1
  92. package/dist/contracts.mjs +0 -365
  93. package/dist/contracts.mjs.map +0 -1
  94. package/dist/index.d.mts +0 -3068
  95. package/dist/index.d.ts +0 -3068
  96. package/dist/index.js +0 -17834
  97. package/dist/index.js.map +0 -1
  98. package/dist/index.mjs +0 -17740
  99. package/dist/index.mjs.map +0 -1
  100. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  101. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  102. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  103. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  104. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  105. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  106. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  107. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  108. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  109. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  110. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  111. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  112. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  113. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  114. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  115. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  116. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  117. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  118. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  119. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  120. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  121. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  122. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  123. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  124. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  125. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  126. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  127. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  128. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  129. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  130. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  131. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  132. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  133. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  134. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  135. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  136. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  137. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
  138. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
  139. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  140. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  141. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  142. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  143. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  144. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  145. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  146. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  147. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  148. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  149. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  150. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  151. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  152. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  153. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  154. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  155. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  156. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  157. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  158. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  159. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
  160. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
  161. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  162. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  163. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  164. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  165. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  166. package/src/blocks/marketing/features/feature-2.tsx +0 -82
  167. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  168. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  169. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  170. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  171. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  172. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  173. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  174. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +0 -38
  175. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  176. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  177. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  178. package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
@@ -95,13 +95,31 @@ export const productDetailProductSchema = z.object({
95
95
  })
96
96
  export type ProductDetailProductSpec = z.infer<typeof productDetailProductSchema>
97
97
 
98
- export const productOverview1PropsSchema = z.object({
98
+ /**
99
+ * Canonical ProductOverview contract. Uses the canonical product projection and adds the
100
+ * bounded `presentation` option plus the `rich` presentation's optional data. Callbacks are not
101
+ * part of the schema (they are runtime props).
102
+ */
103
+ export const productOverviewReviewSchema = z.object({
104
+ id: z.number(),
105
+ author: z.string(),
106
+ rating: z.number(),
107
+ comment: z.string(),
108
+ date: z.string(),
109
+ helpful: z.number().optional(),
110
+ })
111
+
112
+ export const productOverviewPropsSchema = z.object({
99
113
  product: productDetailProductSchema,
114
+ presentation: z.enum(['standard', 'gallery', 'rich']).optional(),
100
115
  selectedVariantId: z.string().nullable().optional(),
101
116
  quantity: z.number().optional(),
102
117
  rating: z.object({ value: z.number(), count: z.number() }).optional(),
103
118
  deliveryEstimate: z.string().optional(),
104
119
  shipping: moneySchema.optional(),
120
+ specifications: z.record(z.string(), z.string()).optional(),
121
+ features: z.array(z.string()).optional(),
122
+ reviews: z.array(productOverviewReviewSchema).optional(),
105
123
  sizesLabel: z.string().optional(),
106
124
  variantLabel: z.string().optional(),
107
125
  reviewsLabel: z.string().optional(),
@@ -112,8 +130,24 @@ export const productOverview1PropsSchema = z.object({
112
130
  quantityLabel: z.string().optional(),
113
131
  deliveryEstimateLabel: z.string().optional(),
114
132
  shippingLabel: z.string().optional(),
133
+ badgeLabel: z.string().optional(),
134
+ availabilityLabel: z.string().optional(),
135
+ skuLabel: z.string().optional(),
136
+ overviewLabel: z.string().optional(),
137
+ specsLabel: z.string().optional(),
138
+ featuresLabel: z.string().optional(),
139
+ specificationsLabel: z.string().optional(),
140
+ galleryLabel: z.string().optional(),
141
+ photosLabel: z.string().optional(),
142
+ saveLabel: z.string().optional(),
143
+ savedLabel: z.string().optional(),
144
+ shareLabel: z.string().optional(),
145
+ basedOnReviewsLabel: z.string().optional(),
146
+ foundHelpfulLabel: z.string().optional(),
147
+ ofCounterLabel: z.string().optional(),
148
+ freeDeliveryLabel: z.string().optional(),
115
149
  })
116
- export type ProductOverview1SpecProps = z.infer<typeof productOverview1PropsSchema>
150
+ export type ProductOverviewSpecProps = z.infer<typeof productOverviewPropsSchema>
117
151
 
118
152
  export const cardProductSchema = z.object({
119
153
  id: z.union([z.string(), z.number()]),
@@ -275,13 +309,6 @@ export const storeContainer1PropsSchema = z.object({
275
309
  })
276
310
  export type StoreContainer1SpecProps = z.infer<typeof storeContainer1PropsSchema>
277
311
 
278
- export const productCard1PropsSchema = z.object({
279
- products: z.array(cardProductSchema),
280
- heading: z.string().optional(),
281
- maxItems: z.number().int().positive().optional(),
282
- })
283
- export type ProductCard1SpecProps = z.infer<typeof productCard1PropsSchema>
284
-
285
312
  /**
286
313
  * Canonical product presentation — the serializable projection a storefront host
287
314
  * produces from a canonical `Product` for the merchandising sections. Unlike
@@ -310,6 +337,42 @@ export const productPresentationSchema = z.object({
310
337
  })
311
338
  export type ProductPresentation = z.infer<typeof productPresentationSchema>
312
339
 
340
+ /**
341
+ * Canonical product card contract (item cell of a product listing). The cell
342
+ * consumes one canonical `ProductPresentation` projection only — the legacy
343
+ * numeric-price card shape is not accepted. `presentation` is the bounded
344
+ * structure switch (`classic`/`minimal`/`rich`) and `wishlisted` is the
345
+ * host-resolved membership state; callbacks and label strings are runtime
346
+ * props and are excluded here. Collection concerns (heading, item cap, loading
347
+ * and empty states) belong to `ProductGrid`.
348
+ */
349
+ export const productCardPropsSchema = z.object({
350
+ item: productPresentationSchema,
351
+ presentation: z.enum(['classic', 'minimal', 'rich']).optional(),
352
+ wishlisted: z.boolean().optional(),
353
+ })
354
+ export type ProductCardSpecProps = z.infer<typeof productCardPropsSchema>
355
+
356
+ /**
357
+ * Canonical product grid contract (listing). Consumes a collection of
358
+ * canonical `ProductPresentation` items and owns the listing's heading, item
359
+ * cap, wishlist membership, and loading/empty/ready states. `wishlistedIds` is
360
+ * resolved per cell before rendering, so the cell receives only its own
361
+ * boolean. Callbacks and label strings are runtime props and are excluded
362
+ * here. Error state belongs to the host.
363
+ */
364
+ export const productGridPropsSchema = z.object({
365
+ items: z.array(productPresentationSchema),
366
+ presentation: z.enum(['classic', 'minimal', 'rich']).optional(),
367
+ heading: z.string().optional(),
368
+ maxItems: z.number().int().positive().optional(),
369
+ wishlistedIds: z.array(z.union([z.string(), z.number()])).readonly().optional(),
370
+ loading: z.boolean().optional(),
371
+ skeletonCount: z.number().int().positive().optional(),
372
+ emptyLabel: z.string().optional(),
373
+ })
374
+ export type ProductGridSpecProps = z.infer<typeof productGridPropsSchema>
375
+
313
376
  /**
314
377
  * Serializable contract for the combined grid-and-carousel product
315
378
  * presentation (`ProductShowcase1`). The grid pages are fixed-size, navigated
@@ -330,10 +393,45 @@ export const productShowcase1PropsSchema = z.object({
330
393
  })
331
394
  export type ProductShowcase1SpecProps = z.infer<typeof productShowcase1PropsSchema>
332
395
 
333
- export const shoppingCart4PropsSchema = z.object({
334
- items: z.array(cartItemSchema),
396
+ /**
397
+ * Canonical shopping cart contract (consolidated shopping-cart family). Lines
398
+ * are keyed by the backend cart line-item id and carry canonical `Money`
399
+ * amounts only — the legacy numeric-price shape is not accepted: a line always
400
+ * carries the backend-authoritative `lineTotal`, and an optional `unitPrice` is
401
+ * carried verbatim (never used for a local `price * quantity` computation).
402
+ * Suggestions fold the numbered carts' recommendations: optional image (omitted
403
+ * when absent) and optional canonical `Money` price. `presentation` is the
404
+ * bounded structure switch (`compact`/`page`); callbacks and interaction state
405
+ * are runtime props and are excluded here.
406
+ */
407
+ export const shoppingCartItemSchema = z.object({
408
+ id: z.string(),
409
+ name: z.string(),
410
+ quantity: z.number().int().min(0),
411
+ /** Optional; absent images are omitted, never serialized as `""`. */
412
+ image: z.string().optional(),
413
+ variant: z.string().optional(),
414
+ /** Canonical backend line total; a malformed amount renders as unavailable. */
415
+ lineTotal: moneySchema,
416
+ /** Canonical backend unit price; carried verbatim, never used locally. */
417
+ unitPrice: moneySchema.optional(),
418
+ })
419
+ export type ShoppingCartItem = z.infer<typeof shoppingCartItemSchema>
420
+
421
+ export const shoppingCartSuggestionSchema = z.object({
422
+ id: z.string(),
423
+ name: z.string(),
424
+ /** Optional; absent images are omitted, never serialized as `""`. */
425
+ image: z.string().optional(),
426
+ unitPrice: moneySchema.optional(),
427
+ })
428
+ export type ShoppingCartSuggestion = z.infer<typeof shoppingCartSuggestionSchema>
429
+
430
+ export const shoppingCartPropsSchema = z.object({
431
+ items: z.array(shoppingCartItemSchema),
335
432
  promotions: z.array(promotionSchema).default([]),
336
- recentlyViewed: z.array(recentlyViewedItemSchema).optional(),
433
+ recentlyViewed: z.array(shoppingCartSuggestionSchema).optional(),
434
+ presentation: z.enum(['compact', 'page']).optional(),
337
435
  /** Backend-authoritative totals; rendered verbatim, never computed locally. */
338
436
  totals: cartTotalsSchema.optional(),
339
437
  totalsState: z.enum(['idle', 'pending', 'error']).optional(),
@@ -341,7 +439,6 @@ export const shoppingCart4PropsSchema = z.object({
341
439
  totalsUnavailableLabel: z.string().optional(),
342
440
  amountUnavailableLabel: z.string().optional(),
343
441
  retryLabel: z.string().optional(),
344
- taxRate: z.number().optional(),
345
442
  titleLabel: z.string().optional(),
346
443
  subtotalLabel: z.string().optional(),
347
444
  discountLabel: z.string().optional(),
@@ -359,6 +456,9 @@ export const shoppingCart4PropsSchema = z.object({
359
456
  estimatedDeliveryLabel: z.string().optional(),
360
457
  secureCheckoutLabel: z.string().optional(),
361
458
  recentlyViewedLabel: z.string().optional(),
459
+ promoPromptLabel: z.string().optional(),
460
+ promoPlaceholderLabel: z.string().optional(),
461
+ promoApplyLabel: z.string().optional(),
362
462
  collapseLabel: z.string().optional(),
363
463
  expandLabel: z.string().optional(),
364
464
  checkoutLabel: z.string().optional(),
@@ -369,7 +469,7 @@ export const shoppingCart4PropsSchema = z.object({
369
469
  decreaseLabel: z.string().optional(),
370
470
  increaseLabel: z.string().optional(),
371
471
  })
372
- export type ShoppingCart4SpecProps = z.infer<typeof shoppingCart4PropsSchema>
472
+ export type ShoppingCartSpecProps = z.infer<typeof shoppingCartPropsSchema>
373
473
 
374
474
  /**
375
475
  * Optional brand seams shared by the existing blocks. These mirror the
@@ -395,7 +495,12 @@ export const heroImageSchema = z.object({
395
495
  })
396
496
  export type HeroImage = z.infer<typeof heroImageSchema>
397
497
 
398
- export const navbar4PropsSchema = z.object({
498
+ /**
499
+ * Serializable contract for the canonical commerce storefront shell
500
+ * (`StorefrontNavbar`). Identity, cart, and search state stay host-owned: the
501
+ * schema carries content and display state only, never cart arithmetic.
502
+ */
503
+ export const storefrontNavbarPropsSchema = z.object({
399
504
  storeCategories: z.array(storeCategorySchema),
400
505
  shippingNote: z.string().optional(),
401
506
  utilityLinks: z.array(utilityLinkSchema),
@@ -419,7 +524,7 @@ export const navbar4PropsSchema = z.object({
419
524
  accountPending: z.boolean().optional(),
420
525
  accountUnavailable: z.boolean().optional(),
421
526
  })
422
- export type Navbar4SpecProps = z.infer<typeof navbar4PropsSchema>
527
+ export type StorefrontNavbarSpecProps = z.infer<typeof storefrontNavbarPropsSchema>
423
528
 
424
529
  /**
425
530
  * Serializable contracts for the two bounded editorial sections (`faq`, `cta`).
@@ -433,6 +538,172 @@ export const ctaSchema = z.object({
433
538
  })
434
539
  export type CtaSpec = z.infer<typeof ctaSchema>
435
540
 
541
+ /** Bounded presentations of the page-level navigation header. */
542
+ export const navbarPresentationSchema = z.enum(['bar', 'mega', 'floating'])
543
+ export type NavbarPresentationSpec = z.infer<typeof navbarPresentationSchema>
544
+
545
+ export const navbarItemSchema = z.object({
546
+ label: z.string(),
547
+ href: z.string(),
548
+ description: z.string().optional(),
549
+ /** Key resolved through the consumer's host-bound `iconMap`. */
550
+ icon: z.string().optional(),
551
+ })
552
+ export type NavbarItemSpec = z.infer<typeof navbarItemSchema>
553
+
554
+ export const navbarGroupSchema = z.object({
555
+ title: z.string().optional(),
556
+ items: z.array(navbarItemSchema),
557
+ })
558
+ export type NavbarGroupSpec = z.infer<typeof navbarGroupSchema>
559
+
560
+ export const navbarLinkSchema = z.object({
561
+ label: z.string(),
562
+ href: z.string().optional(),
563
+ groups: z.array(navbarGroupSchema).optional(),
564
+ emphasis: z.boolean().optional(),
565
+ })
566
+ export type NavbarLinkSpec = z.infer<typeof navbarLinkSchema>
567
+
568
+ export const navbarFeaturedSchema = z.object({
569
+ eyebrow: z.string(),
570
+ title: z.string(),
571
+ description: z.string().optional(),
572
+ href: z.string(),
573
+ linkLabel: z.string().optional(),
574
+ image: z.string().optional(),
575
+ imageAlt: z.string().optional(),
576
+ })
577
+ export type NavbarFeaturedSpec = z.infer<typeof navbarFeaturedSchema>
578
+
579
+ /**
580
+ * Serializable contract for the canonical page-level header (`Navbar`). The
581
+ * serializable subset excludes the React-only bindings: `iconMap`, `onSignIn`,
582
+ * and `onCtaClick` are bound by the host, exactly like the footer's icon map.
583
+ */
584
+ export const navbarPropsSchema = z.object({
585
+ presentation: navbarPresentationSchema.optional(),
586
+ logoText: z.string(),
587
+ logoHref: z.string().optional(),
588
+ currentPath: z.string().optional(),
589
+ links: z.array(navbarLinkSchema),
590
+ featured: navbarFeaturedSchema.optional(),
591
+ signIn: ctaSchema.optional(),
592
+ cta: ctaSchema.optional(),
593
+ /** Consumer-supplied classes merged onto the header surface last. */
594
+ className: z.string().optional(),
595
+ })
596
+ export type NavbarSpecProps = z.infer<typeof navbarPropsSchema>
597
+
598
+ /**
599
+ * Serializable subset of the canonical `StorefrontHero`'s props (callbacks
600
+ * excluded). Every popularity, review, count, or countdown field is optional
601
+ * host-supplied evidence: omitted fields render nothing (honest omission)
602
+ * instead of fabricated defaults, and prices are formatted from the supplied
603
+ * ISO 4217 `currency` via `Intl.NumberFormat` (never hardcoded).
604
+ */
605
+ export const storefrontHeroProductSchema = z.object({
606
+ id: z.string(),
607
+ name: z.string(),
608
+ price: z.number(),
609
+ originalPrice: z.number().optional(),
610
+ image: z.string().optional(),
611
+ imageAlt: z.string().optional(),
612
+ brand: z.string().optional(),
613
+ colors: z.array(z.string()).optional(),
614
+ rating: z.number().optional(),
615
+ reviewCount: z.number().optional(),
616
+ inStock: z.boolean().optional(),
617
+ isNew: z.boolean().optional(),
618
+ tag: z.string().optional(),
619
+ })
620
+ export type StorefrontHeroProduct = z.infer<typeof storefrontHeroProductSchema>
621
+
622
+ export const storefrontHeroPropsSchema = z.object({
623
+ title: z.string(),
624
+ description: z.string(),
625
+ presentation: z.enum(['compact', 'showcase', 'editorial', 'promotional']).optional(),
626
+ currency: z.string().optional(),
627
+ products: z.array(storefrontHeroProductSchema),
628
+ badge: z.string().optional(),
629
+ titleAccent: z.string().optional(),
630
+ primaryCta: ctaSchema.optional(),
631
+ secondaryCta: ctaSchema.optional(),
632
+ heroImage: heroImageSchema.optional(),
633
+ departments: z
634
+ .array(
635
+ z.object({
636
+ id: z.string(),
637
+ name: z.string(),
638
+ image: z.string().optional(),
639
+ imageAlt: z.string().optional(),
640
+ count: z.string().optional(),
641
+ href: z.string().optional(),
642
+ }),
643
+ )
644
+ .optional(),
645
+ featuredDeals: z
646
+ .array(
647
+ z.object({
648
+ id: z.string(),
649
+ name: z.string(),
650
+ description: z.string().optional(),
651
+ image: z.string().optional(),
652
+ imageAlt: z.string().optional(),
653
+ price: z.number(),
654
+ originalPrice: z.number().optional(),
655
+ cta: ctaSchema.optional(),
656
+ }),
657
+ )
658
+ .optional(),
659
+ storeLocator: z
660
+ .object({
661
+ title: z.string(),
662
+ description: z.string(),
663
+ ctaLabel: z.string(),
664
+ ctaHref: z.string().optional(),
665
+ })
666
+ .optional(),
667
+ shippingInfo: z.string().optional(),
668
+ deals: z
669
+ .array(
670
+ z.object({
671
+ id: z.string(),
672
+ title: z.string(),
673
+ discount: z.string().optional(),
674
+ endTime: z.string().optional(),
675
+ }),
676
+ )
677
+ .optional(),
678
+ categories: z
679
+ .array(
680
+ z.object({
681
+ id: z.string(),
682
+ name: z.string(),
683
+ image: z.string().optional(),
684
+ imageAlt: z.string().optional(),
685
+ count: z.string().optional(),
686
+ href: z.string().optional(),
687
+ }),
688
+ )
689
+ .optional(),
690
+ rotationInterval: z.number().optional(),
691
+ addToCartLabel: z.string().optional(),
692
+ buyNowLabel: z.string().optional(),
693
+ newLabel: z.string().optional(),
694
+ outOfStockLabel: z.string().optional(),
695
+ colorsLabel: z.string().optional(),
696
+ discountSuffixLabel: z.string().optional(),
697
+ searchPlaceholder: z.string().optional(),
698
+ searchLabel: z.string().optional(),
699
+ featuredDescription: z.string().optional(),
700
+ shopNowLabel: z.string().optional(),
701
+ endsInLabel: z.string().optional(),
702
+ productsLabel: z.string().optional(),
703
+ viewLabel: z.string().optional(),
704
+ })
705
+ export type StorefrontHeroSpecProps = z.infer<typeof storefrontHeroPropsSchema>
706
+
436
707
  export const faqItemSchema = z.object({
437
708
  value: z.string(),
438
709
  question: z.string(),
@@ -457,6 +728,129 @@ export const ctaSection1PropsSchema = z.object({
457
728
  })
458
729
  export type CtaSection1SpecProps = z.infer<typeof ctaSection1PropsSchema>
459
730
 
731
+ /**
732
+ * Serializable contract for the canonical `Feature` benefits section. The
733
+ * item `icon` is a Lucide icon NAME (e.g. `'Zap'`) that the registry resolves
734
+ * to a component at the seam; the React-only callbacks (`onCtaClick`,
735
+ * `onLearnMore`) are intentionally excluded — the catalog adapter binds them.
736
+ */
737
+ export const featureItemSpecSchema = z.object({
738
+ id: z.string(),
739
+ title: z.string(),
740
+ description: z.string(),
741
+ icon: z.string(),
742
+ })
743
+
744
+ /**
745
+ * `cards` branch: the card grid never owns an image. The presentation is
746
+ * optional so an omitted value resolves to `cards`.
747
+ */
748
+ export const featureCardsSpecSchema = z.object({
749
+ items: z.array(featureItemSpecSchema),
750
+ presentation: z.literal('cards').optional(),
751
+ badge: z.string().optional(),
752
+ title: z.string().optional(),
753
+ description: z.string().optional(),
754
+ ctaLabel: z.string().optional(),
755
+ learnMoreLabel: z.string().optional(),
756
+ className: z.string().optional(),
757
+ })
758
+ export type FeatureCardsSpecProps = z.infer<typeof featureCardsSpecSchema>
759
+
760
+ /**
761
+ * `split` branch: the editorial image is mandatory and must carry a non-empty
762
+ * `src`. `alt` stays required so the caller decides whether the image is
763
+ * informative or decorative. `cards`-only chrome is intentionally absent.
764
+ */
765
+ export const featureSplitSpecSchema = z.object({
766
+ items: z.array(featureItemSpecSchema),
767
+ presentation: z.literal('split'),
768
+ title: z.string().optional(),
769
+ description: z.string().optional(),
770
+ image: z.object({ src: z.string().trim().min(1), alt: z.string() }),
771
+ className: z.string().optional(),
772
+ })
773
+ export type FeatureSplitSpecProps = z.infer<typeof featureSplitSpecSchema>
774
+
775
+ /**
776
+ * Serialized `Feature` union. The split branch is listed first so
777
+ * `presentation: 'split'` resolves deterministically; an omitted presentation
778
+ * falls through to `cards`. A split input without a non-empty image matches
779
+ * neither branch and fails to parse.
780
+ */
781
+ export const featurePropsSchema = z.union([featureSplitSpecSchema, featureCardsSpecSchema])
782
+ export type FeatureSpecProps = z.infer<typeof featurePropsSchema>
783
+
784
+ /**
785
+ * Canonical category contract for the consolidated `ProductCategory` block.
786
+ *
787
+ * `categories` is the canonical category projection (a `@thorprovider/types`
788
+ * `ProductCategory` subset): ids are canonical strings, optional fields are
789
+ * omitted rather than serialized as `""`, and there are no demo-only required
790
+ * fields (no required subtitle, sector icon enum, or Lucide component). `icon`
791
+ * is a plain, serializable icon-key string resolved by the block against a
792
+ * bounded map with a neutral fallback. The `presentation` value is a bounded
793
+ * structure switch, never a per-store fork. Callbacks are runtime props and are
794
+ * excluded here (see the component).
795
+ */
796
+ export const productCategoryItemSchema = z.object({
797
+ id: z.string(),
798
+ handle: z.string().optional(),
799
+ name: z.string(),
800
+ description: z.string().optional(),
801
+ /** Optional real count; an unknown count is omitted, never fabricated as `0`. */
802
+ itemCount: z.number().optional(),
803
+ /** Omitted (never `""`) when the backend has no image. */
804
+ image: z.string().optional(),
805
+ trending: z.boolean().optional(),
806
+ /** Serializable icon key (e.g. `'shirt'`); unknown keys render a neutral fallback. */
807
+ icon: z.string().optional(),
808
+ subcategories: z.array(z.string()).optional(),
809
+ featured: z.boolean().optional(),
810
+ })
811
+ export type ProductCategoryItem = z.infer<typeof productCategoryItemSchema>
812
+
813
+ export const productCategoryPropsSchema = z.object({
814
+ categories: z.array(productCategoryItemSchema),
815
+ presentation: z.enum(['grid', 'icons', 'list']).optional(),
816
+ title: z.string().optional(),
817
+ description: z.string().optional(),
818
+ badge: z.string().optional(),
819
+ cta: ctaSchema.optional(),
820
+ /** Controlled search value; filtering is owned by the host. */
821
+ query: z.string().optional(),
822
+ searchPlaceholder: z.string().optional(),
823
+ searchLabel: z.string().optional(),
824
+ emptyLabel: z.string().optional(),
825
+ noResultsLabel: z.string().optional(),
826
+ browseLabel: z.string().optional(),
827
+ viewAllLabel: z.string().optional(),
828
+ trendingLabel: z.string().optional(),
829
+ featuredLabel: z.string().optional(),
830
+ itemsLabel: z.string().optional(),
831
+ imageFallback: z.string().optional(),
832
+ className: z.string().optional(),
833
+ })
834
+ export type ProductCategorySpecProps = z.infer<typeof productCategoryPropsSchema>
835
+
836
+ export const categoryCardPropsSchema = z.object({
837
+ category: productCategoryItemSchema,
838
+ presentation: z.enum(['grid', 'icons', 'list']).optional(),
839
+ browseLabel: z.string().optional(),
840
+ itemsLabel: z.string().optional(),
841
+ trendingLabel: z.string().optional(),
842
+ featuredLabel: z.string().optional(),
843
+ imageFallback: z.string().optional(),
844
+ })
845
+ export type CategoryCardSpecProps = z.infer<typeof categoryCardPropsSchema>
846
+
847
+ export const categoryGridPropsSchema = productCategoryPropsSchema.extend({
848
+ maxItems: z.number().int().positive().optional(),
849
+ loading: z.boolean().optional(),
850
+ skeletonCount: z.number().int().positive().optional(),
851
+ })
852
+ export type CategoryGridSpecProps = z.infer<typeof categoryGridPropsSchema>
853
+
460
854
  // Pure monetary helpers, re-exported from the server-safe contracts entry so a
461
855
  // host boundary can validate/format `Money` without importing the React entry.
462
856
  export { isCompleteFiniteDecimal, isAvailableMoney, formatMoney } from './money'
@@ -2,7 +2,7 @@ import { cva, type VariantProps } from "class-variance-authority"
2
2
  import { cn } from "cn"
3
3
  import { Slot } from "radix-ui"
4
4
 
5
- import { Separator } from "@/ui/separator"
5
+ import { Separator } from "./separator"
6
6
 
7
7
  const buttonGroupVariants = cva(
8
8
  "flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
@@ -7,7 +7,7 @@ import useEmblaCarousel, {
7
7
  } from "embla-carousel-react"
8
8
  import { ArrowLeft, ArrowRight } from "lucide-react"
9
9
 
10
- import { Button } from "@/ui/button"
10
+ import { Button } from "./button"
11
11
 
12
12
  type CarouselApi = UseEmblaCarouselType[1]
13
13
  type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
package/src/ui/dialog.tsx CHANGED
@@ -5,7 +5,7 @@ import { cn } from "cn"
5
5
  import { XIcon } from "lucide-react"
6
6
  import { Dialog as DialogPrimitive } from "radix-ui"
7
7
 
8
- import { Button } from "@/ui/button"
8
+ import { Button } from "./button"
9
9
 
10
10
  function Dialog({
11
11
  ...props
package/src/ui/field.tsx CHANGED
@@ -4,8 +4,8 @@ import { useMemo } from "react"
4
4
  import { cva, type VariantProps } from "class-variance-authority"
5
5
  import { cn } from "cn"
6
6
 
7
- import { Label } from "@/ui/label"
8
- import { Separator } from "@/ui/separator"
7
+ import { Label } from "./label"
8
+ import { Separator } from "./separator"
9
9
 
10
10
  function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
11
11
  return (
@@ -4,9 +4,9 @@ import * as React from "react"
4
4
  import { cva, type VariantProps } from "class-variance-authority"
5
5
  import { cn } from "cn"
6
6
 
7
- import { Button } from "@/ui/button"
8
- import { Input } from "@/ui/input"
9
- import { Textarea } from "@/ui/textarea"
7
+ import { Button } from "./button"
8
+ import { Input } from "./input"
9
+ import { Textarea } from "./textarea"
10
10
 
11
11
  function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
12
12
  return (
@@ -6,7 +6,7 @@ import {
6
6
  MoreHorizontalIcon,
7
7
  } from "lucide-react"
8
8
 
9
- import { buttonVariants, type Button } from "@/ui/button"
9
+ import { buttonVariants, type Button } from "./button"
10
10
 
11
11
  function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
12
12
  return (
@@ -6,24 +6,24 @@ import { cn } from "cn"
6
6
  import { PanelLeftIcon } from "lucide-react"
7
7
  import { Slot } from "radix-ui"
8
8
 
9
- import { useIsMobile } from "@/hooks/use-mobile"
10
- import { Button } from "@/ui/button"
11
- import { Input } from "@/ui/input"
12
- import { Separator } from "@/ui/separator"
9
+ import { useIsMobile } from "../hooks/use-mobile"
10
+ import { Button } from "./button"
11
+ import { Input } from "./input"
12
+ import { Separator } from "./separator"
13
13
  import {
14
14
  Sheet,
15
15
  SheetContent,
16
16
  SheetDescription,
17
17
  SheetHeader,
18
18
  SheetTitle,
19
- } from "@/ui/sheet"
20
- import { Skeleton } from "@/ui/skeleton"
19
+ } from "./sheet"
20
+ import { Skeleton } from "./skeleton"
21
21
  import {
22
22
  Tooltip,
23
23
  TooltipContent,
24
24
  TooltipProvider,
25
25
  TooltipTrigger,
26
- } from "@/ui/tooltip"
26
+ } from "./tooltip"
27
27
 
28
28
  const SIDEBAR_COOKIE_NAME = "sidebar_state"
29
29
  const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
@@ -5,7 +5,7 @@ import { type VariantProps } from "class-variance-authority"
5
5
  import { cn } from "cn"
6
6
  import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
7
7
 
8
- import { toggleVariants } from "@/ui/toggle"
8
+ import { toggleVariants } from "./toggle"
9
9
 
10
10
  const ToggleGroupContext = React.createContext<
11
11
  VariantProps<typeof toggleVariants> & {
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": false,
5
+ "emitDeclarationOnly": true,
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "incremental": false
9
+ },
10
+ "include": ["src/**/*"],
11
+ "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/*.test.tsx"]
12
+ }
package/tsup.config.ts CHANGED
@@ -1,15 +1,19 @@
1
1
  import { defineConfig } from 'tsup'
2
2
 
3
3
  export default defineConfig({
4
- entry: {
5
- 'index': 'src/index.ts',
6
- 'contracts': 'src/lib/contracts.ts'
7
- },
4
+ // Emit one module per source file (no bundling) so consumers can import a
5
+ // single block and bundle only what they import. `outbase: 'src'` preserves
6
+ // the source layout under dist/ (e.g. dist/blocks/.../navbar.mjs).
7
+ entry: ['src/**/*.ts', 'src/**/*.tsx', '!src/**/*.test.ts', '!src/**/*.test.tsx'],
8
8
  format: ['cjs', 'esm'],
9
- dts: true,
9
+ // Declarations are emitted per-file by `tsc -p tsconfig.build.json` (see the
10
+ // build script); tsup's bundled dts step OOMs with 170+ entries.
11
+ dts: false,
10
12
  splitting: false,
11
13
  sourcemap: true,
12
14
  clean: true,
13
- external: ['react', 'react-dom', 'clsx', 'tailwind-merge', 'lucide-react'],
14
- onSuccess: process.env.SKIP_TYPE_CHECK === 'true' ? undefined : 'tsc --noEmit --skipLibCheck'
15
+ bundle: false,
16
+ esbuildOptions(options) {
17
+ options.outbase = 'src'
18
+ }
15
19
  })